@usamanawaz/react-native-ai-avatar 0.0.0-stage → 1.0.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 +86 -2
- package/assets/models/male-avatar-clothed.glb +0 -0
- package/assets/models/male-avatar.glb +0 -0
- package/dist/AIAvatar.d.ts +20 -0
- package/dist/AvatarView.d.ts +3 -0
- package/dist/animation/animator.d.ts +44 -0
- package/dist/animation/channels.d.ts +32 -0
- package/dist/animation/expressions.d.ts +4 -0
- package/dist/index.d.ts +13 -0
- package/dist/model/bakeSkinning.d.ts +8 -0
- package/dist/model/blendshapes.d.ts +12 -0
- package/dist/model/boneMap.d.ts +9 -0
- package/dist/model/catalog.d.ts +2 -0
- package/dist/model/cpuMorphs.d.ts +24 -0
- package/dist/model/cpuSkin.d.ts +19 -0
- package/dist/model/loadAvatar.d.ts +21 -0
- package/dist/model/skeleton.d.ts +17 -0
- package/dist/model/visemes.d.ts +19 -0
- package/dist/rendering/AvatarStage.d.ts +7 -0
- package/dist/rendering/applyPose.d.ts +3 -0
- package/dist/speech/ExpoSpeechProvider.d.ts +13 -0
- package/dist/speech/approximateMouth.d.ts +15 -0
- package/dist/types.d.ts +35 -0
- package/dist/voice/types.d.ts +18 -0
- package/package.json +72 -4
- package/src/avatar/AIAvatar.tsx +137 -0
- package/src/avatar/AvatarView.tsx +11 -0
- package/src/avatar/animation/animator.ts +344 -0
- package/src/avatar/animation/channels.ts +59 -0
- package/src/avatar/animation/expressions.ts +51 -0
- package/src/avatar/index.ts +23 -0
- package/src/avatar/model/bakeSkinning.ts +46 -0
- package/src/avatar/model/blendshapes.ts +70 -0
- package/src/avatar/model/boneMap.ts +17 -0
- package/src/avatar/model/catalog.ts +15 -0
- package/src/avatar/model/cpuMorphs.ts +124 -0
- package/src/avatar/model/cpuSkin.ts +105 -0
- package/src/avatar/model/jpeg-js.d.ts +6 -0
- package/src/avatar/model/loadAvatar.ts +497 -0
- package/src/avatar/model/skeleton.ts +21 -0
- package/src/avatar/model/visemes.ts +55 -0
- package/src/avatar/rendering/AvatarStage.tsx +119 -0
- package/src/avatar/rendering/applyPose.ts +59 -0
- package/src/avatar/speech/ExpoSpeechProvider.ts +42 -0
- package/src/avatar/speech/approximateMouth.ts +244 -0
- package/src/avatar/types.ts +45 -0
- package/src/avatar/voice/types.ts +20 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Face contract for every GLB this library loads.
|
|
3
|
+
* Names match Apple's ARKit blend shapes so one mesh works for blink,
|
|
4
|
+
* gaze, expression, and lip-sync.
|
|
5
|
+
*/
|
|
6
|
+
export const BLENDSHAPES = [
|
|
7
|
+
'browDownLeft',
|
|
8
|
+
'browDownRight',
|
|
9
|
+
'browInnerUp',
|
|
10
|
+
'browOuterUpLeft',
|
|
11
|
+
'browOuterUpRight',
|
|
12
|
+
'cheekPuff',
|
|
13
|
+
'cheekSquintLeft',
|
|
14
|
+
'cheekSquintRight',
|
|
15
|
+
'eyeBlinkLeft',
|
|
16
|
+
'eyeBlinkRight',
|
|
17
|
+
'eyeLookDownLeft',
|
|
18
|
+
'eyeLookDownRight',
|
|
19
|
+
'eyeLookInLeft',
|
|
20
|
+
'eyeLookInRight',
|
|
21
|
+
'eyeLookOutLeft',
|
|
22
|
+
'eyeLookOutRight',
|
|
23
|
+
'eyeLookUpLeft',
|
|
24
|
+
'eyeLookUpRight',
|
|
25
|
+
'eyeSquintLeft',
|
|
26
|
+
'eyeSquintRight',
|
|
27
|
+
'eyeWideLeft',
|
|
28
|
+
'eyeWideRight',
|
|
29
|
+
'jawForward',
|
|
30
|
+
'jawLeft',
|
|
31
|
+
'jawRight',
|
|
32
|
+
'jawOpen',
|
|
33
|
+
'mouthClose',
|
|
34
|
+
'mouthFunnel',
|
|
35
|
+
'mouthPucker',
|
|
36
|
+
'mouthLeft',
|
|
37
|
+
'mouthRight',
|
|
38
|
+
'mouthSmileLeft',
|
|
39
|
+
'mouthSmileRight',
|
|
40
|
+
'mouthFrownLeft',
|
|
41
|
+
'mouthFrownRight',
|
|
42
|
+
'mouthDimpleLeft',
|
|
43
|
+
'mouthDimpleRight',
|
|
44
|
+
'mouthStretchLeft',
|
|
45
|
+
'mouthStretchRight',
|
|
46
|
+
'mouthRollLower',
|
|
47
|
+
'mouthRollUpper',
|
|
48
|
+
'mouthShrugLower',
|
|
49
|
+
'mouthShrugUpper',
|
|
50
|
+
'mouthPressLeft',
|
|
51
|
+
'mouthPressRight',
|
|
52
|
+
'mouthLowerDownLeft',
|
|
53
|
+
'mouthLowerDownRight',
|
|
54
|
+
'mouthUpperUpLeft',
|
|
55
|
+
'mouthUpperUpRight',
|
|
56
|
+
'noseSneerLeft',
|
|
57
|
+
'noseSneerRight',
|
|
58
|
+
'tongueOut',
|
|
59
|
+
] as const;
|
|
60
|
+
|
|
61
|
+
type AssertLength<T extends readonly unknown[], N extends number> = T['length'] extends N
|
|
62
|
+
? T
|
|
63
|
+
: never;
|
|
64
|
+
|
|
65
|
+
export const ARKIT_BLENDSHAPES: AssertLength<typeof BLENDSHAPES, 52> = BLENDSHAPES;
|
|
66
|
+
|
|
67
|
+
export type BlendshapeName = (typeof BLENDSHAPES)[number];
|
|
68
|
+
|
|
69
|
+
/** Morph target weights. Missing keys stay at 0. Values are 0 to 1. */
|
|
70
|
+
export type BlendshapeWeights = Partial<Record<BlendshapeName, number>>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { BoneName } from './skeleton';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MPFB default-rig names, then the names Three.js actually puts on the
|
|
5
|
+
* objects. Dots are stripped on load (`eye.L` becomes `eyeL`) because they
|
|
6
|
+
* are reserved in animation paths. The GLB itself is left unchanged.
|
|
7
|
+
*/
|
|
8
|
+
export const MPFB_BONE_SOURCES: Record<BoneName, readonly string[]> = {
|
|
9
|
+
Head: ['head', 'Head', 'mixamorigHead'],
|
|
10
|
+
Neck: ['neck01', 'neck02', 'neck03', 'Neck', 'mixamorigNeck'],
|
|
11
|
+
LeftEye: ['eye.L', 'eyeL', 'LeftEye'],
|
|
12
|
+
RightEye: ['eye.R', 'eyeR', 'RightEye'],
|
|
13
|
+
Jaw: ['jaw', 'Jaw'],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** Neck rotation is shared across the three neck bones so one control does not triple the bend. */
|
|
17
|
+
export const NECK_BONE_SHARE = 1 / 3;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AvatarModelId, AvatarOutfit } from '../types';
|
|
2
|
+
|
|
3
|
+
const MODELS: Partial<Record<AvatarModelId, number>> = {
|
|
4
|
+
male: require('../../../assets/models/male-avatar.glb'),
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
const OUTFITS: Record<AvatarOutfit, number> = {
|
|
8
|
+
casual: require('../../../assets/models/male-avatar-clothed.glb'),
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function avatarModelSource(id: AvatarModelId, outfit?: AvatarOutfit): number | null {
|
|
12
|
+
if (outfit) return OUTFITS[outfit] ?? null;
|
|
13
|
+
return MODELS[id] ?? null;
|
|
14
|
+
}
|
|
15
|
+
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
|
|
3
|
+
import type { BlendshapeWeights } from './blendshapes';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Expo GL on iOS (especially the Simulator) often cannot sample the float
|
|
7
|
+
* DataArrayTexture Three.js uses for morph targets. We strip GPU morphs and
|
|
8
|
+
* apply the same deltas on the CPU so the body mesh still draws.
|
|
9
|
+
*/
|
|
10
|
+
export type CpuMorphMesh = {
|
|
11
|
+
mesh: THREE.Mesh;
|
|
12
|
+
dictionary: Record<string, number>;
|
|
13
|
+
basePosition: Float32Array;
|
|
14
|
+
baseNormal: Float32Array | null;
|
|
15
|
+
positionDeltas: Float32Array[];
|
|
16
|
+
normalDeltas: Float32Array[] | null;
|
|
17
|
+
position: THREE.BufferAttribute;
|
|
18
|
+
normal: THREE.BufferAttribute | null;
|
|
19
|
+
scratchPosition: Float32Array;
|
|
20
|
+
scratchNormal: Float32Array | null;
|
|
21
|
+
lastKey: string;
|
|
22
|
+
/** When set, expression offsets are stored here so CPU skinning can run after them. */
|
|
23
|
+
morphOut?: Float32Array;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function extractCpuMorphs(mesh: THREE.Mesh): CpuMorphMesh | null {
|
|
27
|
+
const dictionary = mesh.morphTargetDictionary;
|
|
28
|
+
const geometry = mesh.geometry;
|
|
29
|
+
const morphPositions = geometry.morphAttributes.position;
|
|
30
|
+
if (!dictionary || !morphPositions || morphPositions.length === 0) return null;
|
|
31
|
+
|
|
32
|
+
const position = geometry.getAttribute('position') as THREE.BufferAttribute;
|
|
33
|
+
const normal = geometry.getAttribute('normal') as THREE.BufferAttribute | null;
|
|
34
|
+
const morphNormals = geometry.morphAttributes.normal;
|
|
35
|
+
|
|
36
|
+
const basePosition = floatCopy(position);
|
|
37
|
+
const baseNormal = normal ? floatCopy(normal) : null;
|
|
38
|
+
const positionDeltas = morphPositions.map((attr) => floatCopy(attr as THREE.BufferAttribute));
|
|
39
|
+
const normalDeltas =
|
|
40
|
+
normal && morphNormals && morphNormals.length === morphPositions.length
|
|
41
|
+
? morphNormals.map((attr) => floatCopy(attr as THREE.BufferAttribute))
|
|
42
|
+
: null;
|
|
43
|
+
|
|
44
|
+
// Drop GPU morphs so Three.js never uploads a float morph texture.
|
|
45
|
+
geometry.morphAttributes = {};
|
|
46
|
+
delete mesh.morphTargetDictionary;
|
|
47
|
+
delete mesh.morphTargetInfluences;
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
mesh,
|
|
51
|
+
dictionary: { ...dictionary },
|
|
52
|
+
basePosition,
|
|
53
|
+
baseNormal,
|
|
54
|
+
positionDeltas,
|
|
55
|
+
normalDeltas,
|
|
56
|
+
position,
|
|
57
|
+
normal,
|
|
58
|
+
scratchPosition: new Float32Array(basePosition.length),
|
|
59
|
+
scratchNormal: baseNormal ? new Float32Array(baseNormal.length) : null,
|
|
60
|
+
lastKey: '',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function applyCpuMorphs(targets: CpuMorphMesh[], weights: BlendshapeWeights) {
|
|
65
|
+
const key = weightKey(weights);
|
|
66
|
+
for (const target of targets) {
|
|
67
|
+
if (target.lastKey === key) continue;
|
|
68
|
+
target.lastKey = key;
|
|
69
|
+
const { scratchPosition, basePosition, positionDeltas, dictionary } = target;
|
|
70
|
+
scratchPosition.set(basePosition);
|
|
71
|
+
|
|
72
|
+
for (const [name, weight] of Object.entries(weights)) {
|
|
73
|
+
if (!weight) continue;
|
|
74
|
+
const index = dictionary[name];
|
|
75
|
+
if (index === undefined) continue;
|
|
76
|
+
const delta = positionDeltas[index];
|
|
77
|
+
if (!delta) continue;
|
|
78
|
+
const amount = weight;
|
|
79
|
+
for (let i = 0; i < scratchPosition.length; i += 1) {
|
|
80
|
+
scratchPosition[i] += delta[i] * amount;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (target.morphOut) {
|
|
85
|
+
const morph = target.morphOut;
|
|
86
|
+
const base = target.basePosition;
|
|
87
|
+
for (let i = 0; i < scratchPosition.length; i += 1) morph[i] = scratchPosition[i] - base[i];
|
|
88
|
+
} else {
|
|
89
|
+
(target.position.array as Float32Array).set(scratchPosition);
|
|
90
|
+
target.position.needsUpdate = true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (target.scratchNormal && target.baseNormal && target.normal && target.normalDeltas) {
|
|
94
|
+
const scratchNormal = target.scratchNormal;
|
|
95
|
+
scratchNormal.set(target.baseNormal);
|
|
96
|
+
for (const [name, weight] of Object.entries(weights)) {
|
|
97
|
+
if (!weight) continue;
|
|
98
|
+
const index = dictionary[name];
|
|
99
|
+
if (index === undefined) continue;
|
|
100
|
+
const delta = target.normalDeltas[index];
|
|
101
|
+
if (!delta) continue;
|
|
102
|
+
for (let i = 0; i < scratchNormal.length; i += 1) {
|
|
103
|
+
scratchNormal[i] += delta[i] * weight;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
(target.normal.array as Float32Array).set(scratchNormal);
|
|
107
|
+
target.normal.needsUpdate = true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function floatCopy(attr: THREE.BufferAttribute): Float32Array {
|
|
113
|
+
const array = attr.array;
|
|
114
|
+
return array instanceof Float32Array ? new Float32Array(array) : Float32Array.from(array as ArrayLike<number>);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function weightKey(weights: BlendshapeWeights): string {
|
|
118
|
+
const parts: string[] = [];
|
|
119
|
+
for (const [name, weight] of Object.entries(weights)) {
|
|
120
|
+
if (!weight || weight < 0.001) continue;
|
|
121
|
+
parts.push(`${name}:${weight.toFixed(3)}`);
|
|
122
|
+
}
|
|
123
|
+
return parts.join('|');
|
|
124
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Expo GL cannot sample the float bone texture Three.js uses for SkinnedMesh.
|
|
5
|
+
* The mesh is drawn as a plain Mesh, and bone motion is applied on the CPU.
|
|
6
|
+
*/
|
|
7
|
+
export type CpuSkin = {
|
|
8
|
+
position: THREE.BufferAttribute;
|
|
9
|
+
bind: Float32Array;
|
|
10
|
+
/** Added to the bind position before skinning. Face shapes write this. */
|
|
11
|
+
morph: Float32Array;
|
|
12
|
+
skinIndex: THREE.BufferAttribute;
|
|
13
|
+
skinWeight: THREE.BufferAttribute;
|
|
14
|
+
skeleton: THREE.Skeleton;
|
|
15
|
+
bindMatrix: THREE.Matrix4;
|
|
16
|
+
bindMatrixInverse: THREE.Matrix4;
|
|
17
|
+
boneMatrices: THREE.Matrix4[];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const composed = new THREE.Matrix4();
|
|
21
|
+
|
|
22
|
+
export function createCpuSkins(scene: THREE.Object3D): CpuSkin[] {
|
|
23
|
+
const skinned: THREE.SkinnedMesh[] = [];
|
|
24
|
+
scene.traverse((object) => {
|
|
25
|
+
if ((object as THREE.SkinnedMesh).isSkinnedMesh) skinned.push(object as THREE.SkinnedMesh);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const skins: CpuSkin[] = [];
|
|
29
|
+
for (const source of skinned) {
|
|
30
|
+
const parent = source.parent;
|
|
31
|
+
const geometry = source.geometry;
|
|
32
|
+
const position = geometry.getAttribute('position') as THREE.BufferAttribute | undefined;
|
|
33
|
+
const skinIndex = geometry.getAttribute('skinIndex') as THREE.BufferAttribute | undefined;
|
|
34
|
+
const skinWeight = geometry.getAttribute('skinWeight') as THREE.BufferAttribute | undefined;
|
|
35
|
+
if (!parent || !source.skeleton || !position || !skinIndex || !skinWeight) continue;
|
|
36
|
+
|
|
37
|
+
source.updateMatrixWorld(true);
|
|
38
|
+
const mesh = new THREE.Mesh(geometry, source.material);
|
|
39
|
+
mesh.name = source.name;
|
|
40
|
+
mesh.frustumCulled = false;
|
|
41
|
+
mesh.position.copy(source.position);
|
|
42
|
+
mesh.quaternion.copy(source.quaternion);
|
|
43
|
+
mesh.scale.copy(source.scale);
|
|
44
|
+
mesh.morphTargetDictionary = source.morphTargetDictionary ? { ...source.morphTargetDictionary } : undefined;
|
|
45
|
+
mesh.morphTargetInfluences = source.morphTargetInfluences ? source.morphTargetInfluences.slice() : undefined;
|
|
46
|
+
parent.add(mesh);
|
|
47
|
+
parent.remove(source);
|
|
48
|
+
|
|
49
|
+
skins.push({
|
|
50
|
+
position,
|
|
51
|
+
bind: Float32Array.from(position.array as ArrayLike<number>),
|
|
52
|
+
morph: new Float32Array(position.count * 3),
|
|
53
|
+
skinIndex,
|
|
54
|
+
skinWeight,
|
|
55
|
+
skeleton: source.skeleton,
|
|
56
|
+
bindMatrix: source.bindMatrix.clone(),
|
|
57
|
+
bindMatrixInverse: source.bindMatrixInverse.clone(),
|
|
58
|
+
boneMatrices: source.skeleton.bones.map(() => new THREE.Matrix4()),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return skins;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function updateCpuSkins(skins: CpuSkin[]) {
|
|
65
|
+
for (const skin of skins) {
|
|
66
|
+
const { skeleton, boneMatrices, bindMatrix, bindMatrixInverse } = skin;
|
|
67
|
+
const bones = skeleton.bones;
|
|
68
|
+
const inverses = skeleton.boneInverses;
|
|
69
|
+
for (let bone = 0; bone < bones.length; bone += 1) {
|
|
70
|
+
composed.multiplyMatrices(bones[bone].matrixWorld, inverses[bone]);
|
|
71
|
+
composed.multiply(bindMatrix);
|
|
72
|
+
boneMatrices[bone].multiplyMatrices(bindMatrixInverse, composed);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const out = skin.position.array as Float32Array;
|
|
76
|
+
const bind = skin.bind;
|
|
77
|
+
const morph = skin.morph;
|
|
78
|
+
const indices = skin.skinIndex.array as ArrayLike<number>;
|
|
79
|
+
const weights = skin.skinWeight.array as ArrayLike<number>;
|
|
80
|
+
const stride = skin.skinIndex.itemSize;
|
|
81
|
+
const count = skin.position.count;
|
|
82
|
+
|
|
83
|
+
for (let vertex = 0; vertex < count; vertex += 1) {
|
|
84
|
+
const x = bind[vertex * 3] + morph[vertex * 3];
|
|
85
|
+
const y = bind[vertex * 3 + 1] + morph[vertex * 3 + 1];
|
|
86
|
+
const z = bind[vertex * 3 + 2] + morph[vertex * 3 + 2];
|
|
87
|
+
let ox = 0;
|
|
88
|
+
let oy = 0;
|
|
89
|
+
let oz = 0;
|
|
90
|
+
const base = vertex * stride;
|
|
91
|
+
for (let influence = 0; influence < stride; influence += 1) {
|
|
92
|
+
const weight = weights[base + influence];
|
|
93
|
+
if (weight === 0) continue;
|
|
94
|
+
const elements = boneMatrices[indices[base + influence]].elements;
|
|
95
|
+
ox += weight * (elements[0] * x + elements[4] * y + elements[8] * z + elements[12]);
|
|
96
|
+
oy += weight * (elements[1] * x + elements[5] * y + elements[9] * z + elements[13]);
|
|
97
|
+
oz += weight * (elements[2] * x + elements[6] * y + elements[10] * z + elements[14]);
|
|
98
|
+
}
|
|
99
|
+
out[vertex * 3] = ox;
|
|
100
|
+
out[vertex * 3 + 1] = oy;
|
|
101
|
+
out[vertex * 3 + 2] = oz;
|
|
102
|
+
}
|
|
103
|
+
skin.position.needsUpdate = true;
|
|
104
|
+
}
|
|
105
|
+
}
|