@sage-rsc/talking-head-react 1.3.7 → 1.3.9

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.
@@ -0,0 +1,4825 @@
1
+ /**
2
+ * MIT License
3
+ *
4
+ * Copyright (c) 2024 Mika Suominen
5
+ *
6
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ * of this software and associated documentation files (the "Software"), to deal
8
+ * in the Software without restriction, including without limitation the rights
9
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ * copies of the Software, and to permit persons to whom the Software is
11
+ * furnished to do so, subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be included in all
14
+ * copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ * SOFTWARE.
23
+ */
24
+
25
+ import * as THREE from 'three';
26
+ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
27
+ import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
28
+ import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
29
+ import { FBXLoader } from 'three/addons/loaders/FBXLoader.js';
30
+ import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
31
+ import Stats from 'three/addons/libs/stats.module.js';
32
+
33
+ import{ DynamicBones } from './dynamicbones.mjs';
34
+ const workletUrl = new URL('./playback-worklet.js', import.meta.url);
35
+
36
+ // Temporary objects for animation loop
37
+ const q = new THREE.Quaternion();
38
+ const e = new THREE.Euler();
39
+ const v = new THREE.Vector3();
40
+ const w = new THREE.Vector3();
41
+ const box = new THREE.Box3();
42
+ const m = new THREE.Matrix4();
43
+ const minv = new THREE.Matrix4();
44
+ const origin = new THREE.Vector3();
45
+ const forward = new THREE.Vector3(0, 0, 1);
46
+ const axisx = new THREE.Vector3(1, 0, 0);
47
+ const axisy = new THREE.Vector3(0, 1, 0);
48
+ const axisz = new THREE.Vector3(0, 0, 1);
49
+
50
+ class TalkingHead {
51
+
52
+ /**
53
+ * Avatar.
54
+ * @typedef {Object} Avatar
55
+ * @property {string} url URL for the GLB file
56
+ * @property {string} [body] Body form 'M' or 'F'
57
+ * @property {string} [lipsyncLang] Lip-sync language, e.g. 'fi', 'en'
58
+ * @property {string} [ttsLang] Text-to-speech language, e.g. "fi-FI"
59
+ * @property {voice} [ttsVoice] Voice name.
60
+ * @property {numeric} [ttsRate] Voice rate.
61
+ * @property {numeric} [ttsPitch] Voice pitch.
62
+ * @property {numeric} [ttsVolume] Voice volume.
63
+ * @property {string} [avatarMood] Initial mood.
64
+ * @property {boolean} [avatarMute] If true, muted.
65
+ * @property {numeric} [avatarIdleEyeContact] Eye contact while idle [0,1]
66
+ * @property {numeric} [avatarIdleHeadMove] Eye contact while idle [0,1]
67
+ * @property {numeric} [avatarSpeakingEyeContact] Eye contact while speaking [0,1]
68
+ * @property {numeric} [avatarSpeakingHeadMove] Eye contact while speaking [0,1]
69
+ * @property {Object[]} [modelDynamicBones] Config for Dynamic Bones feature
70
+ */
71
+
72
+ /**
73
+ * Loading progress.
74
+ * @callback progressfn
75
+ * @param {string} url URL of the resource
76
+ * @param {Object} event Progress event
77
+ * @param {boolean} event.lengthComputable If false, total is not known
78
+ * @param {number} event.loaded Number of loaded items
79
+ * @param {number} event.total Number of total items
80
+ */
81
+
82
+ /**
83
+ * Callback when new subtitles have been written to the DOM node.
84
+ * @callback subtitlesfn
85
+ * @param {Object} node DOM node
86
+ */
87
+
88
+ /**
89
+ * Callback when the speech queue processes this marker item.
90
+ * @callback markerfn
91
+ */
92
+
93
+ /**
94
+ * Audio object.
95
+ * @typedef {Object} Audio
96
+ * @property {ArrayBuffer|ArrayBuffer[]} audio Audio buffer or array of buffers
97
+ * @property {string[]} words Words
98
+ * @property {number[]} wtimes Starting times of words
99
+ * @property {number[]} wdurations Durations of words
100
+ * @property {string[]} [visemes] Oculus lip-sync viseme IDs
101
+ * @property {number[]} [vtimes] Starting times of visemes
102
+ * @property {number[]} [vdurations] Durations of visemes
103
+ * @property {string[]} [markers] Timed callback functions
104
+ * @property {number[]} [mtimes] Starting times of markers
105
+ */
106
+
107
+ /**
108
+ * Lip-sync object.
109
+ * @typedef {Object} Lipsync
110
+ * @property {string[]} visemes Oculus lip-sync visemes
111
+ * @property {number[]} times Starting times in relative units
112
+ * @property {number[]} durations Durations in relative units
113
+ */
114
+
115
+ /**
116
+ * @constructor
117
+ * @param {Object} node DOM element of the avatar
118
+ * @param {Object} [opt=null] Global/default options
119
+ */
120
+ constructor(node, opt = null ) {
121
+ this.nodeAvatar = node;
122
+ this.opt = {
123
+ jwtGet: null, // Function to get JSON Web Token
124
+ ttsEndpoint: "",
125
+ ttsApikey: null,
126
+ ttsTrimStart: 0,
127
+ ttsTrimEnd: 400,
128
+ ttsLang: "fi-FI",
129
+ ttsVoice: "fi-FI-Standard-A",
130
+ ttsRate: 1,
131
+ ttsPitch: 0,
132
+ ttsVolume: 0,
133
+ mixerGainSpeech: null,
134
+ mixerGainBackground: null,
135
+ lipsyncLang: 'fi',
136
+ lipsyncModules: ['fi','en','lt'],
137
+ pcmSampleRate: 22050,
138
+ modelRoot: "Armature",
139
+ modelPixelRatio: 1,
140
+ modelFPS: 30,
141
+ modelMovementFactor: 1,
142
+ cameraView: 'full',
143
+ dracoEnabled: false,
144
+ dracoDecoderPath: 'https://www.gstatic.com/draco/v1/decoders/',
145
+ cameraDistance: 0,
146
+ cameraX: 0,
147
+ cameraY: 0,
148
+ cameraRotateX: 0,
149
+ cameraRotateY: 0,
150
+ cameraRotateEnable: true,
151
+ cameraPanEnable: false,
152
+ cameraZoomEnable: false,
153
+ lightAmbientColor: 0xffffff,
154
+ lightAmbientIntensity: 2,
155
+ lightDirectColor: 0x8888aa,
156
+ lightDirectIntensity: 30,
157
+ lightDirectPhi: 1,
158
+ lightDirectTheta: 2,
159
+ lightSpotIntensity: 0,
160
+ lightSpotColor: 0x3388ff,
161
+ lightSpotPhi: 0.1,
162
+ lightSpotTheta: 4,
163
+ lightSpotDispersion: 1,
164
+ avatarMood: "neutral",
165
+ avatarMute: false,
166
+ avatarIdleEyeContact: 0.2,
167
+ avatarIdleHeadMove: 0.5,
168
+ avatarSpeakingEyeContact: 0.5,
169
+ avatarSpeakingHeadMove: 0.5,
170
+ avatarIgnoreCamera: false,
171
+ listeningSilenceThresholdLevel: 40,
172
+ listeningSilenceThresholdMs: 2000,
173
+ listeningSilenceDurationMax: 10000,
174
+ listeningActiveThresholdLevel: 75,
175
+ listeningActiveThresholdMs: 300,
176
+ listeningActiveDurationMax: 240000,
177
+ update: null,
178
+ avatarOnly: false,
179
+ avatarOnlyScene: null,
180
+ avatarOnlyCamera: null,
181
+ statsNode: null,
182
+ statsStyle: null
183
+ };
184
+ Object.assign( this.opt, opt || {} );
185
+
186
+ // Statistics
187
+ if ( this.opt.statsNode ) {
188
+ this.stats = new Stats();
189
+ if ( this.opt.statsStyle ) {
190
+ this.stats.dom.style.cssText = this.opt.statsStyle;
191
+ }
192
+ this.opt.statsNode.appendChild( this.stats.dom );
193
+ }
194
+
195
+ // Pose templates
196
+ // NOTE: The body weight on each pose should be on left foot
197
+ // for most natural result.
198
+ this.poseTemplates = {
199
+ 'side': {
200
+ standing: true,
201
+ props: {
202
+ 'Hips.position':{x:0, y:1, z:0}, 'Hips.rotation':{x:-0.003, y:-0.017, z:0.1}, 'Spine.rotation':{x:-0.103, y:-0.002, z:-0.063}, 'Spine1.rotation':{x:0.042, y:-0.02, z:-0.069}, 'Spine2.rotation':{x:0.131, y:-0.012, z:-0.065}, 'Neck.rotation':{x:0.027, y:0.006, z:0}, 'Head.rotation':{x:0.077, y:-0.065, z:0}, 'LeftShoulder.rotation':{x:1.599, y:0.084, z:-1.77}, 'LeftArm.rotation':{x:1.364, y:0.052, z:-0.044}, 'LeftForeArm.rotation':{x:0.002, y:-0.007, z:0.331}, 'LeftHand.rotation':{x:0.104, y:-0.067, z:-0.174}, 'LeftHandThumb1.rotation':{x:0.231, y:0.258, z:0.355}, 'LeftHandThumb2.rotation':{x:-0.106, y:-0.339, z:-0.454}, 'LeftHandThumb3.rotation':{x:-0.02, y:-0.142, z:-0.004}, 'LeftHandIndex1.rotation':{x:0.148, y:0.032, z:-0.069}, 'LeftHandIndex2.rotation':{x:0.326, y:-0.049, z:-0.029}, 'LeftHandIndex3.rotation':{x:0.247, y:-0.053, z:-0.073}, 'LeftHandMiddle1.rotation':{x:0.238, y:-0.057, z:-0.089}, 'LeftHandMiddle2.rotation':{x:0.469, y:-0.036, z:-0.081}, 'LeftHandMiddle3.rotation':{x:0.206, y:-0.015, z:-0.017}, 'LeftHandRing1.rotation':{x:0.187, y:-0.118, z:-0.157}, 'LeftHandRing2.rotation':{x:0.579, y:0.02, z:-0.097}, 'LeftHandRing3.rotation':{x:0.272, y:0.021, z:-0.063}, 'LeftHandPinky1.rotation':{x:0.405, y:-0.182, z:-0.138}, 'LeftHandPinky2.rotation':{x:0.613, y:0.128, z:-0.144}, 'LeftHandPinky3.rotation':{x:0.268, y:0.094, z:-0.081}, 'RightShoulder.rotation':{x:1.541, y:0.192, z:1.775}, 'RightArm.rotation':{x:1.273, y:-0.352, z:-0.067}, 'RightForeArm.rotation':{x:-0.011, y:-0.031, z:-0.357}, 'RightHand.rotation':{x:-0.008, y:0.312, z:-0.028}, 'RightHandThumb1.rotation':{x:0.23, y:-0.258, z:-0.355}, 'RightHandThumb2.rotation':{x:-0.107, y:0.339, z:0.454}, 'RightHandThumb3.rotation':{x:-0.02, y:0.142, z:0.004}, 'RightHandIndex1.rotation':{x:0.148, y:-0.031, z:0.069}, 'RightHandIndex2.rotation':{x:0.326, y:0.049, z:0.029}, 'RightHandIndex3.rotation':{x:0.247, y:0.053, z:0.073}, 'RightHandMiddle1.rotation':{x:0.237, y:0.057, z:0.089}, 'RightHandMiddle2.rotation':{x:0.469, y:0.036, z:0.081}, 'RightHandMiddle3.rotation':{x:0.206, y:0.015, z:0.017}, 'RightHandRing1.rotation':{x:0.204, y:0.086, z:0.135}, 'RightHandRing2.rotation':{x:0.579, y:-0.02, z:0.098}, 'RightHandRing3.rotation':{x:0.272, y:-0.021, z:0.063}, 'RightHandPinky1.rotation':{x:0.404, y:0.182, z:0.137}, 'RightHandPinky2.rotation':{x:0.613, y:-0.128, z:0.144}, 'RightHandPinky3.rotation':{x:0.268, y:-0.094, z:0.081}, 'LeftUpLeg.rotation':{x:0.096, y:0.209, z:2.983}, 'LeftLeg.rotation':{x:-0.053, y:0.042, z:-0.017}, 'LeftFoot.rotation':{x:1.091, y:0.15, z:0.026}, 'LeftToeBase.rotation':{x:0.469, y:-0.07, z:-0.015}, 'RightUpLeg.rotation':{x:-0.307, y:-0.219, z:2.912}, 'RightLeg.rotation':{x:-0.359, y:0.164, z:0.015}, 'RightFoot.rotation':{x:1.035, y:0.11, z:0.005}, 'RightToeBase.rotation':{x:0.467, y:0.07, z:0.015}
203
+ }
204
+ },
205
+
206
+ 'hip':{
207
+ standing: true,
208
+ props: {
209
+ 'Hips.position':{x:0,y:1,z:0}, 'Hips.rotation':{x:-0.036,y:0.09,z:0.135}, 'Spine.rotation':{x:0.076,y:-0.035,z:0.01}, 'Spine1.rotation':{x:-0.096,y:0.013,z:-0.094}, 'Spine2.rotation':{x:-0.014,y:0.002,z:-0.097}, 'Neck.rotation':{x:0.034,y:-0.051,z:-0.075}, 'Head.rotation':{x:0.298,y:-0.1,z:0.154}, 'LeftShoulder.rotation':{x:1.694,y:0.011,z:-1.68}, 'LeftArm.rotation':{x:1.343,y:0.177,z:-0.153}, 'LeftForeArm.rotation':{x:-0.049,y:0.134,z:0.351}, 'LeftHand.rotation':{x:0.057,y:-0.189,z:-0.026}, 'LeftHandThumb1.rotation':{x:0.368,y:-0.066,z:0.438}, 'LeftHandThumb2.rotation':{x:-0.156,y:0.029,z:-0.369}, 'LeftHandThumb3.rotation':{x:0.034,y:-0.009,z:0.016}, 'LeftHandIndex1.rotation':{x:0.157,y:-0.002,z:-0.171}, 'LeftHandIndex2.rotation':{x:0.099,y:0,z:0}, 'LeftHandIndex3.rotation':{x:0.1,y:0,z:0}, 'LeftHandMiddle1.rotation':{x:0.222,y:-0.019,z:-0.16}, 'LeftHandMiddle2.rotation':{x:0.142,y:0,z:0}, 'LeftHandMiddle3.rotation':{x:0.141,y:0,z:0}, 'LeftHandRing1.rotation':{x:0.333,y:-0.039,z:-0.174}, 'LeftHandRing2.rotation':{x:0.214,y:0,z:0}, 'LeftHandRing3.rotation':{x:0.213,y:0,z:0}, 'LeftHandPinky1.rotation':{x:0.483,y:-0.069,z:-0.189}, 'LeftHandPinky2.rotation':{x:0.312,y:0,z:0}, 'LeftHandPinky3.rotation':{x:0.309,y:0,z:0}, 'RightShoulder.rotation':{x:1.597,y:0.012,z:1.816}, 'RightArm.rotation':{x:0.618,y:-1.274,z:-0.266}, 'RightForeArm.rotation':{x:-0.395,y:-0.097,z:-1.342}, 'RightHand.rotation':{x:-0.816,y:-0.057,z:-0.976}, 'RightHandThumb1.rotation':{x:0.42,y:0.23,z:-1.172}, 'RightHandThumb2.rotation':{x:-0.027,y:0.361,z:0.122}, 'RightHandThumb3.rotation':{x:0.076,y:0.125,z:-0.371}, 'RightHandIndex1.rotation':{x:-0.158,y:-0.045,z:0.033}, 'RightHandIndex2.rotation':{x:0.391,y:0.051,z:0.025}, 'RightHandIndex3.rotation':{x:0.317,y:0.058,z:0.07}, 'RightHandMiddle1.rotation':{x:0.486,y:0.066,z:0.014}, 'RightHandMiddle2.rotation':{x:0.718,y:0.055,z:0.07}, 'RightHandMiddle3.rotation':{x:0.453,y:0.019,z:0.013}, 'RightHandRing1.rotation':{x:0.591,y:0.241,z:0.11}, 'RightHandRing2.rotation':{x:1.014,y:0.023,z:0.097}, 'RightHandRing3.rotation':{x:0.708,y:0.008,z:0.066}, 'RightHandPinky1.rotation':{x:1.02,y:0.305,z:0.051}, 'RightHandPinky2.rotation':{x:1.187,y:-0.028,z:0.191}, 'RightHandPinky3.rotation':{x:0.872,y:-0.031,z:0.121}, 'LeftUpLeg.rotation':{x:-0.095,y:-0.058,z:-3.338}, 'LeftLeg.rotation':{x:-0.366,y:0.287,z:-0.021}, 'LeftFoot.rotation':{x:1.131,y:0.21,z:0.176}, 'LeftToeBase.rotation':{x:0.739,y:-0.068,z:-0.001}, 'RightUpLeg.rotation':{x:-0.502,y:0.362,z:3.153}, 'RightLeg.rotation':{x:-1.002,y:0.109,z:0.008}, 'RightFoot.rotation':{x:0.626,y:-0.097,z:-0.194}, 'RightToeBase.rotation':{x:1.33,y:0.288,z:-0.078}
210
+ }
211
+ },
212
+
213
+ 'turn':{
214
+ standing: true,
215
+ props: {
216
+ 'Hips.position':{x:0,y:1,z:0}, 'Hips.rotation':{x:-0.07,y:-0.604,z:-0.004}, 'Spine.rotation':{x:-0.007,y:0.003,z:0.071}, 'Spine1.rotation':{x:-0.053,y:0.024,z:-0.06}, 'Spine2.rotation':{x:0.074,y:0.013,z:-0.068}, 'Neck.rotation':{x:0.03,y:0.186,z:-0.077}, 'Head.rotation':{x:0.045,y:0.243,z:-0.086}, 'LeftShoulder.rotation':{x:1.717,y:-0.085,z:-1.761}, 'LeftArm.rotation':{x:1.314,y:0.07,z:-0.057}, 'LeftForeArm.rotation':{x:-0.151,y:0.714,z:0.302}, 'LeftHand.rotation':{x:-0.069,y:0.003,z:-0.118}, 'LeftHandThumb1.rotation':{x:0.23,y:0.258,z:0.354}, 'LeftHandThumb2.rotation':{x:-0.107,y:-0.338,z:-0.455}, 'LeftHandThumb3.rotation':{x:-0.015,y:-0.142,z:0.002}, 'LeftHandIndex1.rotation':{x:0.145,y:0.032,z:-0.069}, 'LeftHandIndex2.rotation':{x:0.323,y:-0.049,z:-0.028}, 'LeftHandIndex3.rotation':{x:0.249,y:-0.053,z:-0.074}, 'LeftHandMiddle1.rotation':{x:0.235,y:-0.057,z:-0.088}, 'LeftHandMiddle2.rotation':{x:0.468,y:-0.036,z:-0.081}, 'LeftHandMiddle3.rotation':{x:0.203,y:-0.015,z:-0.017}, 'LeftHandRing1.rotation':{x:0.185,y:-0.118,z:-0.157}, 'LeftHandRing2.rotation':{x:0.578,y:0.02,z:-0.097}, 'LeftHandRing3.rotation':{x:0.27,y:0.021,z:-0.063}, 'LeftHandPinky1.rotation':{x:0.404,y:-0.182,z:-0.138}, 'LeftHandPinky2.rotation':{x:0.612,y:0.128,z:-0.144}, 'LeftHandPinky3.rotation':{x:0.267,y:0.094,z:-0.081}, 'RightShoulder.rotation':{x:1.605,y:0.17,z:1.625}, 'RightArm.rotation':{x:1.574,y:-0.655,z:0.388}, 'RightForeArm.rotation':{x:-0.36,y:-0.849,z:-0.465}, 'RightHand.rotation':{x:0.114,y:0.416,z:-0.069}, 'RightHandThumb1.rotation':{x:0.486,y:0.009,z:-0.492}, 'RightHandThumb2.rotation':{x:-0.073,y:-0.01,z:0.284}, 'RightHandThumb3.rotation':{x:-0.054,y:-0.006,z:0.209}, 'RightHandIndex1.rotation':{x:0.245,y:-0.014,z:0.052}, 'RightHandIndex2.rotation':{x:0.155,y:0,z:0}, 'RightHandIndex3.rotation':{x:0.153,y:0,z:0}, 'RightHandMiddle1.rotation':{x:0.238,y:0.004,z:0.028}, 'RightHandMiddle2.rotation':{x:0.15,y:0,z:0}, 'RightHandMiddle3.rotation':{x:0.149,y:0,z:0}, 'RightHandRing1.rotation':{x:0.267,y:0.012,z:0.007}, 'RightHandRing2.rotation':{x:0.169,y:0,z:0}, 'RightHandRing3.rotation':{x:0.167,y:0,z:0}, 'RightHandPinky1.rotation':{x:0.304,y:0.018,z:-0.021}, 'RightHandPinky2.rotation':{x:0.192,y:0,z:0}, 'RightHandPinky3.rotation':{x:0.19,y:0,z:0}, 'LeftUpLeg.rotation':{x:-0.001,y:-0.058,z:-3.238}, 'LeftLeg.rotation':{x:-0.29,y:0.058,z:-0.021}, 'LeftFoot.rotation':{x:1.288,y:0.168,z:0.183}, 'LeftToeBase.rotation':{x:0.363,y:-0.09,z:-0.01}, 'RightUpLeg.rotation':{x:-0.100,y:0.36,z:3.062}, 'RightLeg.rotation':{x:-0.67,y:-0.304,z:0.043}, 'RightFoot.rotation':{x:1.195,y:-0.159,z:-0.294}, 'RightToeBase.rotation':{x:0.737,y:0.164,z:-0.002}
217
+ }
218
+ },
219
+
220
+ 'bend':{
221
+ bend: true, standing: true,
222
+ props: {
223
+ 'Hips.position':{x:-0.007, y:0.943, z:-0.001}, 'Hips.rotation':{x:1.488, y:-0.633, z:1.435}, 'Spine.rotation':{x:-0.126, y:0.007, z:-0.057}, 'Spine1.rotation':{x:-0.134, y:0.009, z:0.01}, 'Spine2.rotation':{x:-0.019, y:0, z:-0.002}, 'Neck.rotation':{x:-0.159, y:0.572, z:-0.108}, 'Head.rotation':{x:-0.064, y:0.716, z:-0.257}, 'RightShoulder.rotation':{x:1.625, y:-0.043, z:1.382}, 'RightArm.rotation':{x:0.746, y:-0.96, z:-1.009}, 'RightForeArm.rotation':{x:-0.199, y:-0.528, z:-0.38}, 'RightHand.rotation':{x:-0.261, y:-0.043, z:-0.027}, 'RightHandThumb1.rotation':{x:0.172, y:-0.138, z:-0.445}, 'RightHandThumb2.rotation':{x:-0.158, y:0.327, z:0.545}, 'RightHandThumb3.rotation':{x:-0.062, y:0.138, z:0.152}, 'RightHandIndex1.rotation':{x:0.328, y:-0.005, z:0.132}, 'RightHandIndex2.rotation':{x:0.303, y:0.049, z:0.028}, 'RightHandIndex3.rotation':{x:0.241, y:0.046, z:0.077}, 'RightHandMiddle1.rotation':{x:0.309, y:0.074, z:0.089}, 'RightHandMiddle2.rotation':{x:0.392, y:0.036, z:0.081}, 'RightHandMiddle3.rotation':{x:0.199, y:0.014, z:0.019}, 'RightHandRing1.rotation':{x:0.239, y:0.143, z:0.091}, 'RightHandRing2.rotation':{x:0.275, y:-0.02, z:0.097}, 'RightHandRing3.rotation':{x:0.248, y:-0.023, z:0.061}, 'RightHandPinky1.rotation':{x:0.211, y:0.154, z:0.029}, 'RightHandPinky2.rotation':{x:0.348, y:-0.128, z:0.144}, 'RightHandPinky3.rotation':{x:0.21, y:-0.091, z:0.065}, 'LeftShoulder.rotation':{x:1.626, y:-0.027, z:-1.367}, 'LeftArm.rotation':{x:1.048, y:0.737, z:0.712}, 'LeftForeArm.rotation':{x:-0.508, y:0.879, z:0.625}, 'LeftHand.rotation':{x:0.06, y:-0.243, z:-0.079}, 'LeftHandThumb1.rotation':{x:0.187, y:-0.072, z:0.346}, 'LeftHandThumb2.rotation':{x:-0.066, y:0.008, z:-0.256}, 'LeftHandThumb3.rotation':{x:-0.085, y:0.014, z:-0.334}, 'LeftHandIndex1.rotation':{x:-0.1, y:0.016, z:-0.058}, 'LeftHandIndex2.rotation':{x:0.334, y:0, z:0}, 'LeftHandIndex3.rotation':{x:0.281, y:0, z:0}, 'LeftHandMiddle1.rotation':{x:-0.056, y:0, z:0}, 'LeftHandMiddle2.rotation':{x:0.258, y:0, z:0}, 'LeftHandMiddle3.rotation':{x:0.26, y:0, z:0}, 'LeftHandRing1.rotation':{x:-0.067, y:-0.002, z:0.008}, 'LeftHandRing2.rotation':{x:0.259, y:0, z:0}, 'LeftHandRing3.rotation':{x:0.276, y:0, z:0}, 'LeftHandPinky1.rotation':{x:-0.128, y:-0.007, z:0.042}, 'LeftHandPinky2.rotation':{x:0.227, y:0, z:0}, 'LeftHandPinky3.rotation':{x:0.145, y:0, z:0}, 'RightUpLeg.rotation':{x:-1.507, y:0.2, z:-3.043}, 'RightLeg.rotation':{x:-0.689, y:-0.124, z:0.017}, 'RightFoot.rotation':{x:0.909, y:0.008, z:-0.093}, 'RightToeBase.rotation':{x:0.842, y:0.075, z:-0.008}, 'LeftUpLeg.rotation':{x:-1.449, y:-0.2, z:3.018}, 'LeftLeg.rotation':{x:-0.74, y:-0.115, z:-0.008}, 'LeftFoot.rotation':{x:1.048, y:-0.058, z:0.117}, 'LeftToeBase.rotation':{x:0.807, y:-0.067, z:0.003}
224
+ }
225
+ },
226
+
227
+ 'back':{
228
+ standing: true,
229
+ props: {
230
+ 'Hips.position':{x:0,y:1,z:0}, 'Hips.rotation':{x:-0.732,y:-1.463,z:-0.637}, 'Spine.rotation':{x:-0.171,y:0.106,z:0.157}, 'Spine1.rotation':{x:-0.044,y:0.138,z:-0.059}, 'Spine2.rotation':{x:0.082,y:0.133,z:-0.074}, 'Neck.rotation':{x:0.39,y:0.591,z:-0.248}, 'Head.rotation':{x:-0.001,y:0.596,z:-0.057}, 'LeftShoulder.rotation':{x:1.676,y:0.007,z:-1.892}, 'LeftArm.rotation':{x:-5.566,y:1.188,z:-0.173}, 'LeftForeArm.rotation':{x:-0.673,y:-0.105,z:1.702}, 'LeftHand.rotation':{x:-0.469,y:-0.739,z:0.003}, 'LeftHandThumb1.rotation':{x:0.876,y:0.274,z:0.793}, 'LeftHandThumb2.rotation':{x:0.161,y:-0.23,z:-0.172}, 'LeftHandThumb3.rotation':{x:0.078,y:0.027,z:0.156}, 'LeftHandIndex1.rotation':{x:-0.085,y:-0.002,z:0.009}, 'LeftHandIndex2.rotation':{x:0.176,y:0,z:-0.002}, 'LeftHandIndex3.rotation':{x:-0.036,y:0.001,z:-0.035}, 'LeftHandMiddle1.rotation':{x:0.015,y:0.144,z:-0.076}, 'LeftHandMiddle2.rotation':{x:0.378,y:-0.007,z:-0.077}, 'LeftHandMiddle3.rotation':{x:-0.141,y:-0.001,z:0.031}, 'LeftHandRing1.rotation':{x:0.039,y:0.02,z:-0.2}, 'LeftHandRing2.rotation':{x:0.25,y:-0.002,z:-0.073}, 'LeftHandRing3.rotation':{x:0.236,y:0.006,z:-0.075}, 'LeftHandPinky1.rotation':{x:0.172,y:-0.033,z:-0.275}, 'LeftHandPinky2.rotation':{x:0.216,y:0.043,z:-0.054}, 'LeftHandPinky3.rotation':{x:0.325,y:0.078,z:-0.13}, 'RightShoulder.rotation':{x:2.015,y:-0.168,z:1.706}, 'RightArm.rotation':{x:0.203,y:-1.258,z:-0.782}, 'RightForeArm.rotation':{x:-0.658,y:-0.133,z:-1.401}, 'RightHand.rotation':{x:-1.504,y:0.375,z:-0.005}, 'RightHandThumb1.rotation':{x:0.413,y:-0.158,z:-1.121}, 'RightHandThumb2.rotation':{x:-0.142,y:-0.008,z:0.209}, 'RightHandThumb3.rotation':{x:-0.091,y:0.021,z:0.142}, 'RightHandIndex1.rotation':{x:-0.167,y:0.014,z:-0.072}, 'RightHandIndex2.rotation':{x:0.474,y:0.009,z:0.051}, 'RightHandIndex3.rotation':{x:0.115,y:0.006,z:0.047}, 'RightHandMiddle1.rotation':{x:0.385,y:0.019,z:0.144}, 'RightHandMiddle2.rotation':{x:0.559,y:0.035,z:0.101}, 'RightHandMiddle3.rotation':{x:0.229,y:0,z:0.027}, 'RightHandRing1.rotation':{x:0.48,y:0.026,z:0.23}, 'RightHandRing2.rotation':{x:0.772,y:0.038,z:0.109}, 'RightHandRing3.rotation':{x:0.622,y:0.039,z:0.106}, 'RightHandPinky1.rotation':{x:0.767,y:0.288,z:0.353}, 'RightHandPinky2.rotation':{x:0.886,y:0.049,z:0.122}, 'RightHandPinky3.rotation':{x:0.662,y:0.044,z:0.113}, 'LeftUpLeg.rotation':{x:-0.206,y:-0.268,z:-3.343}, 'LeftLeg.rotation':{x:-0.333,y:0.757,z:-0.043}, 'LeftFoot.rotation':{x:1.049,y:0.167,z:0.287}, 'LeftToeBase.rotation':{x:0.672,y:-0.069,z:-0.004}, 'RightUpLeg.rotation':{x:0.055,y:-0.226,z:3.037}, 'RightLeg.rotation':{x:-0.559,y:0.39,z:-0.001}, 'RightFoot.rotation':{x:1.2,y:0.133,z:0.085}, 'RightToeBase.rotation':{x:0.92,y:0.093,z:-0.013}
231
+ }
232
+ },
233
+
234
+ 'straight':{
235
+ standing: true,
236
+ props: {
237
+ 'Hips.position':{x:0, y:0.989, z:0.001}, 'Hips.rotation':{x:0.047, y:0.007, z:-0.007}, 'Spine.rotation':{x:-0.143, y:-0.007, z:0.005}, 'Spine1.rotation':{x:-0.043, y:-0.014, z:0.012}, 'Spine2.rotation':{x:0.072, y:-0.013, z:0.013}, 'Neck.rotation':{x:0.048, y:-0.003, z:0.012}, 'Head.rotation':{x:0.05, y:-0.02, z:-0.017}, 'LeftShoulder.rotation':{x:1.62, y:-0.166, z:-1.605}, 'LeftArm.rotation':{x:1.275, y:0.544, z:-0.092}, 'LeftForeArm.rotation':{x:0, y:0, z:0.302}, 'LeftHand.rotation':{x:-0.225, y:-0.154, z:0.11}, 'LeftHandThumb1.rotation':{x:0.435, y:-0.044, z:0.457}, 'LeftHandThumb2.rotation':{x:-0.028, y:0.002, z:-0.246}, 'LeftHandThumb3.rotation':{x:-0.236, y:-0.025, z:0.113}, 'LeftHandIndex1.rotation':{x:0.218, y:0.008, z:-0.081}, 'LeftHandIndex2.rotation':{x:0.165, y:-0.001, z:-0.017}, 'LeftHandIndex3.rotation':{x:0.165, y:-0.001, z:-0.017}, 'LeftHandMiddle1.rotation':{x:0.235, y:-0.011, z:-0.065}, 'LeftHandMiddle2.rotation':{x:0.182, y:-0.002, z:-0.019}, 'LeftHandMiddle3.rotation':{x:0.182, y:-0.002, z:-0.019}, 'LeftHandRing1.rotation':{x:0.316, y:-0.017, z:0.008}, 'LeftHandRing2.rotation':{x:0.253, y:-0.003, z:-0.026}, 'LeftHandRing3.rotation':{x:0.255, y:-0.003, z:-0.026}, 'LeftHandPinky1.rotation':{x:0.336, y:-0.062, z:0.088}, 'LeftHandPinky2.rotation':{x:0.276, y:-0.004, z:-0.028}, 'LeftHandPinky3.rotation':{x:0.276, y:-0.004, z:-0.028}, 'RightShoulder.rotation':{x:1.615, y:0.064, z:1.53}, 'RightArm.rotation':{x:1.313, y:-0.424, z:0.131}, 'RightForeArm.rotation':{x:0, y:0, z:-0.317}, 'RightHand.rotation':{x:-0.158, y:-0.639, z:-0.196}, 'RightHandThumb1.rotation':{x:0.44, y:0.048, z:-0.549}, 'RightHandThumb2.rotation':{x:-0.056, y:-0.008, z:0.274}, 'RightHandThumb3.rotation':{x:-0.258, y:0.031, z:-0.095}, 'RightHandIndex1.rotation':{x:0.169, y:-0.011, z:0.105}, 'RightHandIndex2.rotation':{x:0.134, y:0.001, z:0.011}, 'RightHandIndex3.rotation':{x:0.134, y:0.001, z:0.011}, 'RightHandMiddle1.rotation':{x:0.288, y:0.014, z:0.092}, 'RightHandMiddle2.rotation':{x:0.248, y:0.003, z:0.02}, 'RightHandMiddle3.rotation':{x:0.249, y:0.003, z:0.02}, 'RightHandRing1.rotation':{x:0.369, y:0.019, z:0.006}, 'RightHandRing2.rotation':{x:0.321, y:0.004, z:0.026}, 'RightHandRing3.rotation':{x:0.323, y:0.004, z:0.026}, 'RightHandPinky1.rotation':{x:0.468, y:0.085, z:-0.03}, 'RightHandPinky2.rotation':{x:0.427, y:0.007, z:0.034}, 'RightHandPinky3.rotation':{x:0.142, y:0.001, z:0.012}, 'LeftUpLeg.rotation':{x:-0.077, y:-0.058, z:3.126}, 'LeftLeg.rotation':{x:-0.252, y:0.001, z:-0.018}, 'LeftFoot.rotation':{x:1.315, y:-0.064, z:0.315}, 'LeftToeBase.rotation':{x:0.577, y:-0.07, z:-0.009}, 'RightUpLeg.rotation':{x:-0.083, y:-0.032, z:3.124}, 'RightLeg.rotation':{x:-0.272, y:-0.003, z:0.021}, 'RightFoot.rotation':{x:1.342, y:0.076, z:-0.222}, 'RightToeBase.rotation':{x:0.44, y:0.069, z:0.016}
238
+ }
239
+ },
240
+
241
+ 'wide':{
242
+ standing: true,
243
+ props: {
244
+ 'Hips.position':{x:0, y:1.017, z:0.016}, 'Hips.rotation':{x:0.064, y:-0.048, z:0.059}, 'Spine.rotation':{x:-0.123, y:0, z:-0.018}, 'Spine1.rotation':{x:0.014, y:0.003, z:-0.006}, 'Spine2.rotation':{x:0.04, y:0.003, z:-0.007}, 'Neck.rotation':{x:0.101, y:0.007, z:-0.035}, 'Head.rotation':{x:-0.091, y:-0.049, z:0.105}, 'RightShoulder.rotation':{x:1.831, y:0.017, z:1.731}, 'RightArm.rotation':{x:-1.673, y:-1.102, z:-3.132}, 'RightForeArm.rotation':{x:0.265, y:0.23, z:-0.824}, 'RightHand.rotation':{x:-0.52, y:0.345, z:-0.061}, 'RightHandThumb1.rotation':{x:0.291, y:0.056, z:-0.428}, 'RightHandThumb2.rotation':{x:0.025, y:0.005, z:0.166}, 'RightHandThumb3.rotation':{x:-0.089, y:0.009, z:0.068}, 'RightHandIndex1.rotation':{x:0.392, y:-0.015, z:0.11}, 'RightHandIndex2.rotation':{x:0.391, y:0.001, z:0.004}, 'RightHandIndex3.rotation':{x:0.326, y:0, z:0.003}, 'RightHandMiddle1.rotation':{x:0.285, y:0.068, z:0.081}, 'RightHandMiddle2.rotation':{x:0.519, y:0.004, z:0.011}, 'RightHandMiddle3.rotation':{x:0.252, y:0, z:0.001}, 'RightHandRing1.rotation':{x:0.207, y:0.133, z:0.146}, 'RightHandRing2.rotation':{x:0.597, y:0.004, z:0.004}, 'RightHandRing3.rotation':{x:0.292, y:0.002, z:0.012}, 'RightHandPinky1.rotation':{x:0.338, y:0.182, z:0.136}, 'RightHandPinky2.rotation':{x:0.533, y:0.002, z:0.004}, 'RightHandPinky3.rotation':{x:0.194, y:0, z:0.002}, 'LeftShoulder.rotation':{x:1.83, y:-0.063, z:-1.808}, 'LeftArm.rotation':{x:-1.907, y:1.228, z:-2.959}, 'LeftForeArm.rotation':{x:-0.159, y:0.268, z:0.572}, 'LeftHand.rotation':{x:0.069, y:-0.498, z:-0.025}, 'LeftHandThumb1.rotation':{x:0.738, y:0.123, z:0.178}, 'LeftHandThumb2.rotation':{x:-0.26, y:0.028, z:-0.477}, 'LeftHandThumb3.rotation':{x:-0.448, y:0.093, z:-0.661}, 'LeftHandIndex1.rotation':{x:1.064, y:0.005, z:-0.13}, 'LeftHandIndex2.rotation':{x:1.55, y:-0.143, z:-0.136}, 'LeftHandIndex3.rotation':{x:0.722, y:-0.076, z:-0.127}, 'LeftHandMiddle1.rotation':{x:1.095, y:-0.091, z:0.006}, 'LeftHandMiddle2.rotation':{x:1.493, y:-0.174, z:-0.151}, 'LeftHandMiddle3.rotation':{x:0.651, y:-0.031, z:-0.087}, 'LeftHandRing1.rotation':{x:1.083, y:-0.224, z:0.072}, 'LeftHandRing2.rotation':{x:1.145, y:-0.107, z:-0.195}, 'LeftHandRing3.rotation':{x:1.208, y:-0.134, z:-0.158}, 'LeftHandPinky1.rotation':{x:0.964, y:-0.383, z:0.128}, 'LeftHandPinky2.rotation':{x:1.457, y:-0.146, z:-0.159}, 'LeftHandPinky3.rotation':{x:1.019, y:-0.102, z:-0.141}, 'RightUpLeg.rotation':{x:-0.221, y:-0.233, z:2.87}, 'RightLeg.rotation':{x:-0.339, y:-0.043, z:-0.041}, 'RightFoot.rotation':{x:1.081, y:0.177, z:0.114}, 'RightToeBase.rotation':{x:0.775, y:0, z:0}, 'LeftUpLeg.rotation':{x:-0.185, y:0.184, z:3.131}, 'LeftLeg.rotation':{x:-0.408, y:0.129, z:0.02}, 'LeftFoot.rotation':{x:1.167, y:-0.002, z:-0.007}, 'LeftToeBase.rotation':{x:0.723, y:0, z:0}
245
+ }
246
+ },
247
+
248
+ 'oneknee':{
249
+ kneeling: true,
250
+ props: {
251
+ 'Hips.position':{x:-0.005, y:0.415, z:-0.017}, 'Hips.rotation':{x:-0.25, y:0.04, z:-0.238}, 'Spine.rotation':{x:0.037, y:0.043, z:0.047}, 'Spine1.rotation':{x:0.317, y:0.103, z:0.066}, 'Spine2.rotation':{x:0.433, y:0.109, z:0.054}, 'Neck.rotation':{x:-0.156, y:-0.092, z:0.059}, 'Head.rotation':{x:-0.398, y:-0.032, z:0.018}, 'RightShoulder.rotation':{x:1.546, y:0.119, z:1.528}, 'RightArm.rotation':{x:0.896, y:-0.247, z:-0.512}, 'RightForeArm.rotation':{x:0.007, y:0, z:-1.622}, 'RightHand.rotation':{x:1.139, y:-0.853, z:0.874}, 'RightHandThumb1.rotation':{x:0.176, y:0.107, z:-0.311}, 'RightHandThumb2.rotation':{x:-0.047, y:-0.003, z:0.12}, 'RightHandThumb3.rotation':{x:0, y:0, z:0}, 'RightHandIndex1.rotation':{x:0.186, y:0.005, z:0.125}, 'RightHandIndex2.rotation':{x:0.454, y:0.005, z:0.015}, 'RightHandIndex3.rotation':{x:0, y:0, z:0}, 'RightHandMiddle1.rotation':{x:0.444, y:0.035, z:0.127}, 'RightHandMiddle2.rotation':{x:0.403, y:-0.006, z:-0.04}, 'RightHandMiddle3.rotation':{x:0, y:0, z:0}, 'RightHandRing1.rotation':{x:0.543, y:0.074, z:0.121}, 'RightHandRing2.rotation':{x:0.48, y:-0.018, z:-0.063}, 'RightHandRing3.rotation':{x:0, y:0, z:0}, 'RightHandPinky1.rotation':{x:0.464, y:0.086, z:0.113}, 'RightHandPinky2.rotation':{x:0.667, y:-0.06, z:-0.128}, 'RightHandPinky3.rotation':{x:0, y:0, z:0}, 'LeftShoulder.rotation':{x:1.545, y:-0.116, z:-1.529}, 'LeftArm.rotation':{x:0.799, y:0.631, z:0.556}, 'LeftForeArm.rotation':{x:-0.002, y:0.007, z:0.926}, 'LeftHand.rotation':{x:-0.508, y:0.439, z:0.502}, 'LeftHandThumb1.rotation':{x:0.651, y:-0.035, z:0.308}, 'LeftHandThumb2.rotation':{x:-0.053, y:0.008, z:-0.11}, 'LeftHandThumb3.rotation':{x:0, y:0, z:0}, 'LeftHandIndex1.rotation':{x:0.662, y:-0.053, z:-0.116}, 'LeftHandIndex2.rotation':{x:0.309, y:-0.004, z:-0.02}, 'LeftHandIndex3.rotation':{x:0, y:0, z:0}, 'LeftHandMiddle1.rotation':{x:0.501, y:-0.062, z:-0.12}, 'LeftHandMiddle2.rotation':{x:0.144, y:-0.002, z:0.016}, 'LeftHandMiddle3.rotation':{x:0, y:0, z:0}, 'LeftHandRing1.rotation':{x:0.397, y:-0.029, z:-0.143}, 'LeftHandRing2.rotation':{x:0.328, y:0.01, z:0.059}, 'LeftHandRing3.rotation':{x:0, y:0, z:0}, 'LeftHandPinky1.rotation':{x:0.194, y:0.008, z:-0.164}, 'LeftHandPinky2.rotation':{x:0.38, y:0.031, z:0.128}, 'LeftHandPinky3.rotation':{x:0, y:0, z:0}, 'RightUpLeg.rotation':{x:-1.594, y:-0.251, z:2.792}, 'RightLeg.rotation':{x:-2.301, y:-0.073, z:0.055}, 'RightFoot.rotation':{x:1.553, y:-0.207, z:-0.094}, 'RightToeBase.rotation':{x:0.459, y:0.069, z:0.016}, 'LeftUpLeg.rotation':{x:-0.788, y:-0.236, z:-2.881}, 'LeftLeg.rotation':{x:-2.703, y:0.012, z:-0.047}, 'LeftFoot.rotation':{x:2.191, y:-0.102, z:0.019}, 'LeftToeBase.rotation':{x:1.215, y:-0.027, z:0.01}
252
+ }
253
+ },
254
+
255
+ 'kneel':{
256
+ kneeling: true, lying: true,
257
+ props: {
258
+ 'Hips.position':{x:0, y:0.532, z:-0.002}, 'Hips.rotation':{x:0.018, y:-0.008, z:-0.017}, 'Spine.rotation':{x:-0.139, y:-0.01, z:0.002}, 'Spine1.rotation':{x:0.002, y:-0.002, z:0.001}, 'Spine2.rotation':{x:0.028, y:-0.002, z:0.001}, 'Neck.rotation':{x:-0.007, y:0, z:-0.002}, 'Head.rotation':{x:-0.02, y:-0.008, z:-0.004}, 'LeftShoulder.rotation':{x:1.77, y:-0.428, z:-1.588}, 'LeftArm.rotation':{x:0.911, y:0.343, z:0.083}, 'LeftForeArm.rotation':{x:0, y:0, z:0.347}, 'LeftHand.rotation':{x:0.033, y:-0.052, z:-0.105}, 'LeftHandThumb1.rotation':{x:0.508, y:-0.22, z:0.708}, 'LeftHandThumb2.rotation':{x:-0.323, y:-0.139, z:-0.56}, 'LeftHandThumb3.rotation':{x:-0.328, y:0.16, z:-0.301}, 'LeftHandIndex1.rotation':{x:0.178, y:0.248, z:0.045}, 'LeftHandIndex2.rotation':{x:0.236, y:-0.002, z:-0.019}, 'LeftHandIndex3.rotation':{x:-0.062, y:0, z:0.005}, 'LeftHandMiddle1.rotation':{x:0.123, y:-0.005, z:-0.019}, 'LeftHandMiddle2.rotation':{x:0.589, y:-0.014, z:-0.045}, 'LeftHandMiddle3.rotation':{x:0.231, y:-0.002, z:-0.019}, 'LeftHandRing1.rotation':{x:0.196, y:-0.008, z:-0.091}, 'LeftHandRing2.rotation':{x:0.483, y:-0.009, z:-0.038}, 'LeftHandRing3.rotation':{x:0.367, y:-0.005, z:-0.029}, 'LeftHandPinky1.rotation':{x:0.191, y:-0.269, z:-0.246}, 'LeftHandPinky2.rotation':{x:0.37, y:-0.006, z:-0.029}, 'LeftHandPinky3.rotation':{x:0.368, y:-0.005, z:-0.029}, 'RightShoulder.rotation':{x:1.73, y:0.434, z:1.715}, 'RightArm.rotation':{x:0.841, y:-0.508, z:-0.155}, 'RightForeArm.rotation':{x:0, y:0, z:-0.355}, 'RightHand.rotation':{x:0.091, y:0.137, z:0.197}, 'RightHandThumb1.rotation':{x:0.33, y:0.051, z:-0.753}, 'RightHandThumb2.rotation':{x:-0.113, y:0.075, z:0.612}, 'RightHandThumb3.rotation':{x:-0.271, y:-0.166, z:0.164}, 'RightHandIndex1.rotation':{x:0.073, y:0.001, z:-0.093}, 'RightHandIndex2.rotation':{x:0.338, y:0.006, z:0.034}, 'RightHandIndex3.rotation':{x:0.131, y:0.001, z:0.013}, 'RightHandMiddle1.rotation':{x:0.13, y:0.005, z:-0.017}, 'RightHandMiddle2.rotation':{x:0.602, y:0.018, z:0.058}, 'RightHandMiddle3.rotation':{x:-0.031, y:0, z:-0.003}, 'RightHandRing1.rotation':{x:0.351, y:0.019, z:0.045}, 'RightHandRing2.rotation':{x:0.19, y:0.002, z:0.019}, 'RightHandRing3.rotation':{x:0.21, y:0.002, z:0.021}, 'RightHandPinky1.rotation':{x:0.256, y:0.17, z:0.118}, 'RightHandPinky2.rotation':{x:0.451, y:0.01, z:0.045}, 'RightHandPinky3.rotation':{x:0.346, y:0.006, z:0.035}, 'LeftUpLeg.rotation':{x:-0.06, y:0.1, z:-2.918}, 'LeftLeg.rotation':{x:-1.933, y:-0.01, z:0.011}, 'LeftFoot.rotation':{x:0.774, y:-0.162, z:-0.144}, 'LeftToeBase.rotation':{x:1.188, y:0, z:0}, 'RightUpLeg.rotation':{x:-0.099, y:-0.057, z:2.922}, 'RightLeg.rotation':{x:-1.93, y:0.172, z:-0.02}, 'RightFoot.rotation':{x:0.644, y:0.251, z:0.212}, 'RightToeBase.rotation':{x:0.638, y:-0.034, z:-0.001}
259
+ }
260
+ },
261
+
262
+ 'sitting': {
263
+ sitting: true, lying: true,
264
+ props: {
265
+ 'Hips.position':{x:0, y:0.117, z:0.005}, 'Hips.rotation':{x:-0.411, y:-0.049, z:0.056}, 'Spine.rotation':{x:0.45, y:-0.039, z:-0.116}, 'Spine1.rotation':{x:0.092, y:-0.076, z:0.08}, 'Spine2.rotation':{x:0.073, y:0.035, z:0.066}, 'Neck.rotation':{x:0.051, y:0.053, z:-0.079}, 'Head.rotation':{x:-0.169, y:0.009, z:0.034}, 'LeftShoulder.rotation':{x:1.756, y:-0.037, z:-1.301}, 'LeftArm.rotation':{x:-0.098, y:0.016, z:1.006}, 'LeftForeArm.rotation':{x:-0.089, y:0.08, z:0.837}, 'LeftHand.rotation':{x:0.262, y:-0.399, z:0.3}, 'LeftHandThumb1.rotation':{x:0.149, y:-0.043, z:0.452}, 'LeftHandThumb2.rotation':{x:0.032, y:0.006, z:-0.162}, 'LeftHandThumb3.rotation':{x:-0.086, y:-0.005, z:-0.069}, 'LeftHandIndex1.rotation':{x:0.145, y:0.032, z:-0.069}, 'LeftHandIndex2.rotation':{x:0.325, y:-0.001, z:-0.004}, 'LeftHandIndex3.rotation':{x:0.253, y:0, z:-0.003}, 'LeftHandMiddle1.rotation':{x:0.186, y:-0.051, z:-0.091}, 'LeftHandMiddle2.rotation':{x:0.42, y:-0.003, z:-0.011}, 'LeftHandMiddle3.rotation':{x:0.153, y:0.001, z:-0.001}, 'LeftHandRing1.rotation':{x:0.087, y:-0.19, z:-0.078}, 'LeftHandRing2.rotation':{x:0.488, y:-0.004, z:-0.005}, 'LeftHandRing3.rotation':{x:0.183, y:-0.001, z:-0.012}, 'LeftHandPinky1.rotation':{x:0.205, y:-0.262, z:0.051}, 'LeftHandPinky2.rotation':{x:0.407, y:-0.002, z:-0.004}, 'LeftHandPinky3.rotation':{x:0.068, y:0, z:-0.002}, 'RightShoulder.rotation':{x:1.619, y:-0.139, z:1.179}, 'RightArm.rotation':{x:0.17, y:-0.037, z:-1.07}, 'RightForeArm.rotation':{x:-0.044, y:-0.056, z:-0.665}, 'RightHand.rotation':{x:0.278, y:0.454, z:-0.253}, 'RightHandThumb1.rotation':{x:0.173, y:0.089, z:-0.584}, 'RightHandThumb2.rotation':{x:-0.003, y:-0.004, z:0.299}, 'RightHandThumb3.rotation':{x:-0.133, y:-0.002, z:0.235}, 'RightHandIndex1.rotation':{x:0.393, y:-0.023, z:0.108}, 'RightHandIndex2.rotation':{x:0.391, y:0.001, z:0.004}, 'RightHandIndex3.rotation':{x:0.326, y:0, z:0.003}, 'RightHandMiddle1.rotation':{x:0.285, y:0.062, z:0.086}, 'RightHandMiddle2.rotation':{x:0.519, y:0.003, z:0.011}, 'RightHandMiddle3.rotation':{x:0.252, y:-0.001, z:0.001}, 'RightHandRing1.rotation':{x:0.207, y:0.122, z:0.155}, 'RightHandRing2.rotation':{x:0.597, y:0.004, z:0.005}, 'RightHandRing3.rotation':{x:0.292, y:0.001, z:0.012}, 'RightHandPinky1.rotation':{x:0.338, y:0.171, z:0.149}, 'RightHandPinky2.rotation':{x:0.533, y:0.002, z:0.004}, 'RightHandPinky3.rotation':{x:0.194, y:0, z:0.002}, 'LeftUpLeg.rotation':{x:-1.957, y:0.083, z:-2.886}, 'LeftLeg.rotation':{x:-1.46, y:0.123, z:0.005}, 'LeftFoot.rotation':{x:-0.013, y:0.016, z:0.09}, 'LeftToeBase.rotation':{x:0.744, y:0, z:0}, 'RightUpLeg.rotation':{x:-1.994, y:0.125, z:2.905}, 'RightLeg.rotation':{x:-1.5, y:-0.202, z:-0.006}, 'RightFoot.rotation':{x:-0.012, y:-0.065, z:0.081}, 'RightToeBase.rotation':{x:0.758, y:0, z:0}
266
+ }
267
+ }
268
+ };
269
+
270
+ // Gestures
271
+ // NOTE: For one hand gestures, use left left
272
+ this.gestureTemplates = {
273
+ 'handup': {
274
+ 'LeftShoulder.rotation':{x:[1.5,2,1,2], y:[0.2,0.4,1,2], z:[-1.5,-1.3,1,2]}, 'LeftArm.rotation':{x:[1.5,1.7,1,2], y:[-0.6,-0.4,1,2], z:[1,1.2,1,2]}, 'LeftForeArm.rotation':{x:-0.815, y:[-0.4,0,1,2], z:1.575}, 'LeftHand.rotation':{x:-0.529, y:-0.2, z:0.022}, 'LeftHandThumb1.rotation':{x:0.745, y:-0.526, z:0.604}, 'LeftHandThumb2.rotation':{x:-0.107, y:-0.01, z:-0.142}, 'LeftHandThumb3.rotation':{x:0, y:0.001, z:0}, 'LeftHandIndex1.rotation':{x:-0.126, y:-0.035, z:-0.087}, 'LeftHandIndex2.rotation':{x:0.255, y:0.007, z:-0.085}, 'LeftHandIndex3.rotation':{x:0, y:0, z:0}, 'LeftHandMiddle1.rotation':{x:-0.019, y:-0.128, z:-0.082}, 'LeftHandMiddle2.rotation':{x:0.233, y:0.019, z:-0.074}, 'LeftHandMiddle3.rotation':{x:0, y:0, z:0}, 'LeftHandRing1.rotation':{x:0.005, y:-0.241, z:-0.122}, 'LeftHandRing2.rotation':{x:0.261, y:0.021, z:-0.076}, 'LeftHandRing3.rotation':{x:0, y:0, z:0}, 'LeftHandPinky1.rotation':{x:0.059, y:-0.336, z:-0.2}, 'LeftHandPinky2.rotation':{x:0.153, y:0.019, z:0.001}, 'LeftHandPinky3.rotation':{x:0, y:0, z:0}
275
+ },
276
+ 'index': {
277
+ 'LeftShoulder.rotation':{x:[1.5,2,1,2], y:[0.2,0.4,1,2], z:[-1.5,-1.3,1,2]}, 'LeftArm.rotation':{x:[1.5,1.7,1,2], y:[-0.6,-0.4,1,2], z:[1,1.2,1,2]}, 'LeftForeArm.rotation':{x:-0.815, y:[-0.4,0,1,2], z:1.575}, 'LeftHand.rotation':{x:-0.276, y:-0.506, z:-0.208}, 'LeftHandThumb1.rotation':{x:0.579, y:0.228, z:0.363}, 'LeftHandThumb2.rotation':{x:-0.027, y:-0.04, z:-0.662}, 'LeftHandThumb3.rotation':{x:0, y:0.001, z:0}, 'LeftHandIndex1.rotation':{x:0, y:-0.105, z:0.225}, 'LeftHandIndex2.rotation':{x:0.256, y:-0.103, z:-0.213}, 'LeftHandIndex3.rotation':{x:0, y:0, z:0}, 'LeftHandMiddle1.rotation':{x:1.453, y:0.07, z:0.021}, 'LeftHandMiddle2.rotation':{x:1.599, y:0.062, z:0.07}, 'LeftHandMiddle3.rotation':{x:0, y:0, z:0}, 'LeftHandRing1.rotation':{x:1.528, y:-0.073, z:0.052}, 'LeftHandRing2.rotation':{x:1.386, y:0.044, z:0.053}, 'LeftHandRing3.rotation':{x:0, y:0, z:0}, 'LeftHandPinky1.rotation':{x:1.65, y:-0.204, z:0.031}, 'LeftHandPinky2.rotation':{x:1.302, y:0.071, z:0.085}, 'LeftHandPinky3.rotation':{x:0, y:0, z:0}
278
+ },
279
+ 'ok': {
280
+ 'LeftShoulder.rotation':{x:[1.5,2,1,2], y:[0.2,0.4,1,2], z:[-1.5,-1.3,1,2]}, 'LeftArm.rotation':{x:[1.5,1.7,1,1], y:[-0.6,-0.4,1,2], z:[1,1.2,1,2]}, 'LeftForeArm.rotation':{x:-0.415, y:[-0.4,0,1,2], z:1.575}, 'LeftHand.rotation':{x:-0.476, y:-0.506, z:-0.208}, 'LeftHandThumb1.rotation':{x:0.703, y:0.445, z:0.899}, 'LeftHandThumb2.rotation':{x:-0.312, y:-0.04, z:-0.938}, 'LeftHandThumb3.rotation':{x:-0.37, y:0.024, z:-0.393}, 'LeftHandIndex1.rotation':{x:0.8, y:-0.086, z:-0.091}, 'LeftHandIndex2.rotation':{x:1.123, y:-0.046, z:-0.074}, 'LeftHandIndex3.rotation':{x:0.562, y:-0.013, z:-0.043}, 'LeftHandMiddle1.rotation':{x:-0.019, y:-0.128, z:-0.082}, 'LeftHandMiddle2.rotation':{x:0.233, y:0.019, z:-0.074}, 'LeftHandMiddle3.rotation':{x:0, y:0, z:0}, 'LeftHandRing1.rotation':{x:0.005, y:-0.241, z:-0.122}, 'LeftHandRing2.rotation':{x:0.261, y:0.021, z:-0.076}, 'LeftHandRing3.rotation':{x:0, y:0, z:0}, 'LeftHandPinky1.rotation':{x:0.059, y:-0.336, z:-0.2}, 'LeftHandPinky2.rotation':{x:0.153, y:0.019, z:0.001}, 'LeftHandPinky3.rotation':{x:0, y:0, z:0}
281
+ },
282
+ 'thumbup': {
283
+ 'LeftShoulder.rotation':{x:[1.5,2,1,2], y:[0.2,0.4,1,2], z:[-1.5,-1.3,1,2]}, 'LeftArm.rotation':{x:[1.5,1.7,1,2], y:[-0.6,-0.4,1,2], z:[1,1.2,1,2]}, 'LeftForeArm.rotation':{x:-0.415, y:0.206, z:1.575}, 'LeftHand.rotation':{x:-0.276, y:-0.506, z:-0.208}, 'LeftHandThumb1.rotation':{x:0.208, y:-0.189, z:0.685}, 'LeftHandThumb2.rotation':{x:0.129, y:-0.285, z:-0.163}, 'LeftHandThumb3.rotation':{x:-0.047, y:0.068, z:0.401}, 'LeftHandIndex1.rotation':{x:1.412, y:-0.102, z:-0.152}, 'LeftHandIndex2.rotation':{x:1.903, y:-0.16, z:-0.114}, 'LeftHandIndex3.rotation':{x:0.535, y:-0.017, z:-0.062}, 'LeftHandMiddle1.rotation':{x:1.424, y:-0.103, z:-0.12}, 'LeftHandMiddle2.rotation':{x:1.919, y:-0.162, z:-0.114}, 'LeftHandMiddle3.rotation':{x:0.44, y:-0.012, z:-0.051}, 'LeftHandRing1.rotation':{x:1.619, y:-0.127, z:-0.053}, 'LeftHandRing2.rotation':{x:1.898, y:-0.16, z:-0.115}, 'LeftHandRing3.rotation':{x:0.262, y:-0.004, z:-0.031}, 'LeftHandPinky1.rotation':{x:1.661, y:-0.131, z:-0.016}, 'LeftHandPinky2.rotation':{x:1.715, y:-0.067, z:-0.13}, 'LeftHandPinky3.rotation':{x:0.627, y:-0.023, z:-0.071}
284
+ },
285
+ 'thumbdown': {
286
+ 'LeftShoulder.rotation':{x:[1.5,2,1,2], y:[0.2,0.4,1,2], z:[-1.5,-1.3,1,2]}, 'LeftArm.rotation':{x:[1.5,1.7,1,2], y:[-0.6,-0.4,1,2], z:[1,1.2,1,2]}, 'LeftForeArm.rotation':{x:-2.015, y:0.406, z:1.575}, 'LeftHand.rotation':{x:-0.176, y:-0.206, z:-0.208}, 'LeftHandThumb1.rotation':{x:0.208, y:-0.189, z:0.685}, 'LeftHandThumb2.rotation':{x:0.129, y:-0.285, z:-0.163}, 'LeftHandThumb3.rotation':{x:-0.047, y:0.068, z:0.401}, 'LeftHandIndex1.rotation':{x:1.412, y:-0.102, z:-0.152}, 'LeftHandIndex2.rotation':{x:1.903, y:-0.16, z:-0.114}, 'LeftHandIndex3.rotation':{x:0.535, y:-0.017, z:-0.062}, 'LeftHandMiddle1.rotation':{x:1.424, y:-0.103, z:-0.12}, 'LeftHandMiddle2.rotation':{x:1.919, y:-0.162, z:-0.114}, 'LeftHandMiddle3.rotation':{x:0.44, y:-0.012, z:-0.051}, 'LeftHandRing1.rotation':{x:1.619, y:-0.127, z:-0.053}, 'LeftHandRing2.rotation':{x:1.898, y:-0.16, z:-0.115}, 'LeftHandRing3.rotation':{x:0.262, y:-0.004, z:-0.031}, 'LeftHandPinky1.rotation':{x:1.661, y:-0.131, z:-0.016}, 'LeftHandPinky2.rotation':{x:1.715, y:-0.067, z:-0.13}, 'LeftHandPinky3.rotation':{x:0.627, y:-0.023, z:-0.071}
287
+ },
288
+ 'side': {
289
+ 'LeftShoulder.rotation':{x:1.755, y:-0.035, z:-1.63}, 'LeftArm.rotation':{x:1.263, y:-0.955, z:1.024}, 'LeftForeArm.rotation':{x:0, y:0, z:0.8}, 'LeftHand.rotation':{x:-0.36, y:-1.353, z:-0.184}, 'LeftHandThumb1.rotation':{x:0.137, y:-0.049, z:0.863}, 'LeftHandThumb2.rotation':{x:-0.293, y:0.153, z:-0.193}, 'LeftHandThumb3.rotation':{x:-0.271, y:-0.17, z:0.18}, 'LeftHandIndex1.rotation':{x:-0.018, y:0.007, z:0.28}, 'LeftHandIndex2.rotation':{x:0.247, y:-0.003, z:-0.025}, 'LeftHandIndex3.rotation':{x:0.13, y:-0.001, z:-0.013}, 'LeftHandMiddle1.rotation':{x:0.333, y:-0.015, z:0.182}, 'LeftHandMiddle2.rotation':{x:0.313, y:-0.005, z:-0.032}, 'LeftHandMiddle3.rotation':{x:0.294, y:-0.004, z:-0.03}, 'LeftHandRing1.rotation':{x:0.456, y:-0.028, z:-0.092}, 'LeftHandRing2.rotation':{x:0.53, y:-0.014, z:-0.052}, 'LeftHandRing3.rotation':{x:0.478, y:-0.012, z:-0.047}, 'LeftHandPinky1.rotation':{x:0.647, y:-0.049, z:-0.184}, 'LeftHandPinky2.rotation':{x:0.29, y:-0.004, z:-0.029}, 'LeftHandPinky3.rotation':{x:0.501, y:-0.013, z:-0.049}
290
+ },
291
+ 'shrug': {
292
+ 'Neck.rotation':{x:[-0.3,0.3,1,2], y:[-0.3,0.3,1,2], z:[-0.1,0.1]}, 'Head.rotation':{x:[-0.3,0.3], y:[-0.3,0.3], z:[-0.1,0.1]}, 'RightShoulder.rotation':{x:1.732, y:-0.058, z:1.407}, 'RightArm.rotation':{x:1.305, y:0.46, z:0.118}, 'RightForeArm.rotation':{x:[0,2.0], y:[-1,0.2], z:-1.637}, 'RightHand.rotation':{x:-0.048, y:0.165, z:-0.39}, 'RightHandThumb1.rotation':{x:1.467, y:0.599, z:-1.315}, 'RightHandThumb2.rotation':{x:-0.255, y:-0.123, z:0.119}, 'RightHandThumb3.rotation':{x:0, y:-0.002, z:0}, 'RightHandIndex1.rotation':{x:-0.293, y:-0.066, z:-0.112}, 'RightHandIndex2.rotation':{x:0.181, y:0.007, z:0.069}, 'RightHandIndex3.rotation':{x:0, y:0, z:0}, 'RightHandMiddle1.rotation':{x:-0.063, y:-0.041, z:0.032}, 'RightHandMiddle2.rotation':{x:0.149, y:0.005, z:0.05}, 'RightHandMiddle3.rotation':{x:0, y:0, z:0}, 'RightHandRing1.rotation':{x:0.152, y:-0.03, z:0.132}, 'RightHandRing2.rotation':{x:0.194, y:0.007, z:0.058}, 'RightHandRing3.rotation':{x:0, y:0, z:0}, 'RightHandPinky1.rotation':{x:0.306, y:-0.015, z:0.257}, 'RightHandPinky2.rotation':{x:0.15, y:-0.003, z:-0.003}, 'RightHandPinky3.rotation':{x:0, y:0, z:0}, 'LeftShoulder.rotation':{x:1.713, y:0.141, z:-1.433}, 'LeftArm.rotation':{x:1.136, y:-0.422, z:-0.416}, 'LeftForeArm.rotation':{x:1.42, y:0.123, z:1.506}, 'LeftHand.rotation':{x:0.073, y:-0.138, z:0.064}, 'LeftHandThumb1.rotation':{x:1.467, y:-0.599, z:1.314}, 'LeftHandThumb2.rotation':{x:-0.255, y:0.123, z:-0.119}, 'LeftHandThumb3.rotation':{x:0, y:0.001, z:0}, 'LeftHandIndex1.rotation':{x:-0.293, y:0.066, z:0.112}, 'LeftHandIndex2.rotation':{x:0.181, y:-0.007, z:-0.069}, 'LeftHandIndex3.rotation':{x:0, y:0, z:0}, 'LeftHandMiddle1.rotation':{x:-0.062, y:0.041, z:-0.032}, 'LeftHandMiddle2.rotation':{x:0.149, y:-0.005, z:-0.05}, 'LeftHandMiddle3.rotation':{x:0, y:0, z:0}, 'LeftHandRing1.rotation':{x:0.152, y:0.03, z:-0.132}, 'LeftHandRing2.rotation':{x:0.194, y:-0.007, z:-0.058}, 'LeftHandRing3.rotation':{x:0, y:0, z:0}, 'LeftHandPinky1.rotation':{x:0.306, y:0.015, z:-0.257}, 'LeftHandPinky2.rotation':{x:0.15, y:0.003, z:0.003}, 'LeftHandPinky3.rotation':{x:0, y:0, z:0}
293
+ },
294
+ 'namaste': {
295
+ 'RightShoulder.rotation':{x:1.758, y:0.099, z:1.604}, 'RightArm.rotation':{x:0.862, y:-0.292, z:-0.932}, 'RightForeArm.rotation':{x:0.083, y:0.066, z:-1.791}, 'RightHand.rotation':{x:-0.52, y:-0.001, z:-0.176}, 'RightHandThumb1.rotation':{x:0.227, y:0.418, z:-0.776}, 'RightHandThumb2.rotation':{x:-0.011, y:-0.003, z:0.171}, 'RightHandThumb3.rotation':{x:-0.041, y:-0.001, z:-0.013}, 'RightHandIndex1.rotation':{x:-0.236, y:0.003, z:-0.028}, 'RightHandIndex2.rotation':{x:0.004, y:0, z:0.001}, 'RightHandIndex3.rotation':{x:0.002, y:0, z:0}, 'RightHandMiddle1.rotation':{x:-0.236, y:0.003, z:-0.028}, 'RightHandMiddle2.rotation':{x:0.004, y:0, z:0.001}, 'RightHandMiddle3.rotation':{x:0.002, y:0, z:0}, 'RightHandRing1.rotation':{x:-0.236, y:0.003, z:-0.028}, 'RightHandRing2.rotation':{x:0.004, y:0, z:0.001}, 'RightHandRing3.rotation':{x:0.002, y:0, z:0}, 'RightHandPinky1.rotation':{x:-0.236, y:0.003, z:-0.028}, 'RightHandPinky2.rotation':{x:0.004, y:0, z:0.001}, 'RightHandPinky3.rotation':{x:0.002, y:0, z:0}, 'LeftShoulder.rotation':{x:1.711, y:-0.002, z:-1.625}, 'LeftArm.rotation':{x:0.683, y:0.334, z:0.977}, 'LeftForeArm.rotation':{x:0.086, y:-0.066, z:1.843}, 'LeftHand.rotation':{x:-0.595, y:-0.229, z:0.096}, 'LeftHandThumb1.rotation':{x:0.404, y:-0.05, z:0.537}, 'LeftHandThumb2.rotation':{x:-0.02, y:0.004, z:-0.154}, 'LeftHandThumb3.rotation':{x:-0.049, y:0.002, z:-0.019}, 'LeftHandIndex1.rotation':{x:-0.113, y:-0.001, z:0.014}, 'LeftHandIndex2.rotation':{x:0.003, y:0, z:0}, 'LeftHandIndex3.rotation':{x:0.002, y:0, z:0}, 'LeftHandMiddle1.rotation':{x:-0.113, y:-0.001, z:0.014}, 'LeftHandMiddle2.rotation':{x:0.004, y:0, z:0}, 'LeftHandMiddle3.rotation':{x:0.002, y:0, z:0}, 'LeftHandRing1.rotation':{x:-0.113, y:-0.001, z:0.014}, 'LeftHandRing2.rotation':{x:0.003, y:0, z:0}, 'LeftHandRing3.rotation':{x:0.002, y:0, z:0}, 'LeftHandPinky1.rotation':{x:-0.122, y:-0.001, z:-0.057}, 'LeftHandPinky2.rotation':{x:0.012, y:0.001, z:0.07}, 'LeftHandPinky3.rotation':{x:0.002, y:0, z:0}
296
+ }
297
+ }
298
+
299
+
300
+ // Pose deltas
301
+ // NOTE: In this object (x,y,z) are always Euler rotations despite the name!!
302
+ // NOTE: This object should include all the used delta properties.
303
+ this.poseDelta = {
304
+ props: {
305
+ 'Hips.quaternion':{x:0, y:0, z:0},'Spine.quaternion':{x:0, y:0, z:0},
306
+ 'Spine1.quaternion':{x:0, y:0, z:0}, 'Neck.quaternion':{x:0, y:0, z:0},
307
+ 'Head.quaternion':{x:0, y:0, z:0}, 'Spine1.scale':{x:0, y:0, z:0},
308
+ 'Neck.scale':{x:0, y:0, z:0}, 'LeftArm.scale':{x:0, y:0, z:0},
309
+ 'RightArm.scale':{x:0, y:0, z:0}
310
+ }
311
+ };
312
+ // Add legs, arms and hands
313
+ ['Left','Right'].forEach( x => {
314
+ ['Leg','UpLeg','Arm','ForeArm','Hand'].forEach( y => {
315
+ this.poseDelta.props[x+y+'.quaternion'] = {x:0, y:0, z:0};
316
+ });
317
+ ['HandThumb', 'HandIndex','HandMiddle','HandRing', 'HandPinky'].forEach( y => {
318
+ this.poseDelta.props[x+y+'1.quaternion'] = {x:0, y:0, z:0};
319
+ this.poseDelta.props[x+y+'2.quaternion'] = {x:0, y:0, z:0};
320
+ this.poseDelta.props[x+y+'3.quaternion'] = {x:0, y:0, z:0};
321
+ });
322
+ })
323
+
324
+ // Dynamically pick up all the property names that we need in the code
325
+ const names = new Set();
326
+ Object.values(this.poseTemplates).forEach( x => {
327
+ Object.keys( this.propsToThreeObjects(x.props) ).forEach( y => names.add(y) );
328
+ });
329
+ Object.keys( this.poseDelta.props ).forEach( x => {
330
+ names.add(x)
331
+ });
332
+ this.posePropNames = [...names];
333
+
334
+ // Use "side" as the first pose, weight on left leg
335
+ this.poseName = "side"; // First pose
336
+ this.poseWeightOnLeft = true; // Initial weight on left leg
337
+ this.gesture = null; // Values that override pose properties
338
+ this.poseCurrentTemplate = this.poseTemplates[this.poseName];
339
+ this.poseStraight = this.propsToThreeObjects( this.poseTemplates["straight"].props ); // Straight pose used as a reference
340
+ this.poseBase = this.poseFactory( this.poseCurrentTemplate );
341
+ this.poseTarget = this.poseFactory( this.poseCurrentTemplate );
342
+ this.poseAvatar = null; // Set when avatar has been loaded
343
+
344
+ // Avatar height in meters
345
+ // NOTE: The actual value is calculated based on the eye level on avatar load
346
+ this.avatarHeight = 1.7;
347
+
348
+
349
+ // Animation templates
350
+ //
351
+ // baseline: Describes morph target baseline. Values can be either float or
352
+ // an array [start,end,skew] describing a probability distribution.
353
+ // speech : Describes voice rate, pitch and volume as deltas to the values
354
+ // set as options.
355
+ // anims : Animations for breathing, pose, etc. To be used animation
356
+ // sequence is selected in the following order:
357
+ // 1. State (idle, speaking, listening)
358
+ // 2. Mood (moodX, moodY)
359
+ // 3. Pose (poseX, poseY)
360
+ // 5. View (full, upper, head)
361
+ // 6. Body form ('M','F')
362
+ // 7. Alt (sequence of objects with propabilities p. If p is not
363
+ // specified, the remaining part is shared equivally among
364
+ // the rest.)
365
+ // 8. Current object
366
+ // object : delay, delta times dt and values vs.
367
+ //
368
+
369
+ this.animTemplateEyes = { name: 'eyes',
370
+ idle: { alt: [
371
+ {
372
+ p: () => ( this.avatar?.hasOwnProperty('avatarIdleEyeContact') ? this.avatar.avatarIdleEyeContact : this.opt.avatarIdleEyeContact ),
373
+ delay: [200,5000], dt: [ 200,[2000,5000],[3000,10000,1,2] ],
374
+ vs: {
375
+ headMove: [ this.avatar?.hasOwnProperty('avatarIdleHeadMove') ? this.avatar.avatarIdleHeadMove : this.opt.avatarIdleHeadMove ],
376
+ eyesRotateY: [[-0.6,0.6]], eyesRotateX: [[-0.2,0.6]],
377
+ eyeContact: [null,1]
378
+ }
379
+ },
380
+ {
381
+ delay: [200,5000], dt: [ 200,[2000,5000,1,2] ], vs: {
382
+ headMove: [ this.avatar?.hasOwnProperty('avatarIdleHeadMove') ? this.avatar.avatarIdleHeadMove : this.opt.avatarIdleHeadMove ],
383
+ eyesRotateY: [[-0.6,0.6]], eyesRotateX: [[-0.2,0.6]]
384
+ }
385
+ }
386
+ ]},
387
+ speaking: { alt: [
388
+ {
389
+ p: () => ( this.avatar?.hasOwnProperty('avatarSpeakingEyeContact') ? this.avatar.avatarSpeakingEyeContact : this.opt.avatarSpeakingEyeContact ),
390
+ delay: [200,5000], dt: [ 0, [3000,10000,1,2], [2000,5000] ],
391
+ vs: { eyeContact: [1,null],
392
+ headMove: [null,( this.avatar?.hasOwnProperty('avatarSpeakingHeadMove') ? this.avatar.avatarSpeakingHeadMove : this.opt.avatarSpeakingHeadMove ),null],
393
+ eyesRotateY: [null,[-0.6,0.6]], eyesRotateX: [null,[-0.2,0.6]]
394
+ }
395
+ },
396
+ {
397
+ delay: [200,5000], dt: [ 200,[2000,5000,1,2] ], vs: {
398
+ headMove: [( this.avatar?.hasOwnProperty('avatarSpeakingHeadMove') ? this.avatar.avatarSpeakingHeadMove : this.opt.avatarSpeakingHeadMove ),null],
399
+ eyesRotateY: [[-0.6,0.6]], eyesRotateX: [[-0.2,0.6]]
400
+ }
401
+ }
402
+ ]}
403
+ };
404
+ this.animTemplateBlink = { name: 'blink', alt: [
405
+ { p: 0.85, delay: [1000,8000,1,2], dt: [50,[100,300],100], vs: { eyeBlinkLeft: [1,1,0], eyeBlinkRight: [1,1,0] } },
406
+ { delay: [1000,4000,1,2], dt: [50,[100,200],100,[10,400,0],50,[100,200],100], vs: { eyeBlinkLeft: [1,1,0,0,1,1,0], eyeBlinkRight: [1,1,0,0,1,1,0] } }
407
+ ]};
408
+
409
+ this.animMoods = {
410
+ 'neutral' : {
411
+ baseline: { eyesLookDown: 0.1 },
412
+ speech: { deltaRate: 0, deltaPitch: 0, deltaVolume: 0 },
413
+ anims: [
414
+ { name: 'breathing', delay: 1500, dt: [ 1200,500,1000 ], vs: { chestInhale: [0.5,0.5,0] } },
415
+ { name: 'pose', alt: [
416
+ { p: 0.5, delay: [5000,30000], vs: { pose: ['side'] } },
417
+ { p: 0.3, delay: [5000,30000], vs: { pose: ['hip'] },
418
+ 'M': { delay: [5000,30000], vs: { pose: ['wide'] } }
419
+ },
420
+ { delay: [5000,30000], vs: { pose: ['straight'] } }
421
+ ]},
422
+ { name: 'head',
423
+ idle: { delay: [0,1000], dt: [ [200,5000] ], vs: { bodyRotateX: [[-0.04,0.10]], bodyRotateY: [[-0.3,0.3]], bodyRotateZ: [[-0.08,0.08]] } },
424
+ speaking: { dt: [ [0,1000,0] ], vs: { bodyRotateX: [[-0.05,0.15,1,2]], bodyRotateY: [[-0.1,0.1]], bodyRotateZ: [[-0.1,0.1]] } }
425
+ },
426
+ this.animTemplateEyes,
427
+ this.animTemplateBlink,
428
+ { name: 'mouth', delay: [1000,5000], dt: [ [100,500],[100,5000,2] ], vs : { mouthRollLower: [[0,0.3,2]], mouthRollUpper: [[0,0.3,2]], mouthStretchLeft: [[0,0.3]], mouthStretchRight: [[0,0.3]], mouthPucker: [[0,0.3]] } },
429
+ { name: 'misc', delay: [100,5000], dt: [ [100,500],[1000,5000,2] ], vs : { eyeSquintLeft: [[0,0.3,2]], eyeSquintRight: [[0,0.3,2]], browInnerUp: [[0,0.3,2]], browOuterUpLeft: [[0,0.3,2]], browOuterUpRight: [[0,0.3,2]] } }
430
+ ]
431
+ },
432
+ 'happy' : {
433
+ baseline: { mouthSmile: 0.2, eyesLookDown: 0.1 },
434
+ speech: { deltaRate: 0, deltaPitch: 0.1, deltaVolume: 0 },
435
+ anims: [
436
+ { name: 'breathing', delay: 1500, dt: [ 1200,500,1000 ], vs: { chestInhale: [0.5,0.5,0] } },
437
+ { name: 'pose',
438
+ idle: {
439
+ alt: [
440
+ { p: 0.6, delay: [5000,30000], vs: { pose: ['side'] } },
441
+ { p: 0.2, delay: [5000,30000], vs: { pose: ['hip'] },
442
+ 'M': { delay: [5000,30000], vs: { pose: ['side'] } }
443
+ },
444
+ { p: 0.1, delay: [5000,30000], vs: { pose: ['straight'] } },
445
+ { delay: [5000,10000], vs: { pose: ['wide'] } },
446
+ { delay: [1000,3000], vs: { pose: ['turn'] } },
447
+ ]
448
+ },
449
+ speaking: {
450
+ alt: [
451
+ { p: 0.4, delay: [5000,30000], vs: { pose: ['side'] } },
452
+ { p: 0.4, delay: [5000,30000], vs: { pose: ['straight'] } },
453
+ { delay: [5000,20000], vs: { pose: ['hip'] },
454
+ 'M': { delay: [5000,30000], vs: { pose: ['wide'] } }
455
+ },
456
+ ]
457
+ }
458
+ },
459
+ { name: 'head',
460
+ idle: { dt: [ [1000,5000] ], vs: { bodyRotateX: [[-0.04,0.10]], bodyRotateY: [[-0.3,0.3]], bodyRotateZ: [[-0.08,0.08]] } },
461
+ speaking: { dt: [ [0,1000,0] ], vs: { bodyRotateX: [[-0.05,0.15,1,2]], bodyRotateY: [[-0.1,0.1]], bodyRotateZ: [[-0.1,0.1]] } }
462
+ },
463
+ this.animTemplateEyes,
464
+ this.animTemplateBlink,
465
+ { name: 'mouth', delay: [1000,5000], dt: [ [100,500],[100,5000,2] ], vs : { mouthLeft: [[0,0.3,2]], mouthSmile: [[0,0.2,3]], mouthRollLower: [[0,0.3,2]], mouthRollUpper: [[0,0.3,2]], mouthStretchLeft: [[0,0.3]], mouthStretchRight: [[0,0.3]], mouthPucker: [[0,0.3]] } },
466
+ { name: 'misc', delay: [100,5000], dt: [ [100,500],[1000,5000,2] ], vs : { eyeSquintLeft: [[0,0.3,2]], eyeSquintRight: [[0,0.3,2]], browInnerUp: [[0,0.3,2]], browOuterUpLeft: [[0,0.3,2]], browOuterUpRight: [[0,0.3,2]] } }
467
+ ]
468
+ },
469
+ 'angry' : {
470
+ baseline: { eyesLookDown: 0.1, browDownLeft: 0.6, browDownRight: 0.6, jawForward: 0.3, mouthFrownLeft: 0.7, mouthFrownRight: 0.7, mouthRollLower: 0.2, mouthShrugLower: 0.3, handFistLeft: 1, handFistRight: 1 },
471
+ speech: { deltaRate: -0.2, deltaPitch: 0.2, deltaVolume: 0 },
472
+ anims: [
473
+ { name: 'breathing', delay: 500, dt: [ 1000,500,1000 ], vs: { chestInhale: [0.7,0.7,0] } },
474
+ { name: 'pose', alt: [
475
+ { p: 0.4, delay: [5000,30000], vs: { pose: ['side'] } },
476
+ { p: 0.4, delay: [5000,30000], vs: { pose: ['straight'] } },
477
+ { p: 0.2, delay: [5000,30000], vs: { pose: ['hip'] },
478
+ 'M': { delay: [5000,30000], vs: { pose: ['wide'] } }
479
+ },
480
+ ]},
481
+ { name: 'head',
482
+ idle: { delay: [100,500], dt: [ [200,5000] ], vs: { bodyRotateX: [[-0.04,0.10]], bodyRotateY: [[-0.2,0.2]], bodyRotateZ: [[-0.08,0.08]] } },
483
+ speaking: { dt: [ [0,1000,0] ], vs: { bodyRotateX: [[-0.05,0.15,1,2]], bodyRotateY: [[-0.1,0.1]], bodyRotateZ: [[-0.1,0.1]] } }
484
+ },
485
+ this.animTemplateEyes,
486
+ this.animTemplateBlink,
487
+ { name: 'mouth', delay: [1000,5000], dt: [ [100,500],[100,5000,2] ], vs : { mouthRollLower: [[0,0.3,2]], mouthRollUpper: [[0,0.3,2]], mouthStretchLeft: [[0,0.3]], mouthStretchRight: [[0,0.3]], mouthPucker: [[0,0.3]] } },
488
+ { name: 'misc', delay: [100,5000], dt: [ [100,500],[1000,5000,2] ], vs : { eyeSquintLeft: [[0,0.3,2]], eyeSquintRight: [[0,0.3,2]], browInnerUp: [[0,0.3,2]], browOuterUpLeft: [[0,0.3,2]], browOuterUpRight: [[0,0.3,2]] } }
489
+ ]
490
+ },
491
+ 'sad' : {
492
+ baseline: { eyesLookDown: 0.2, browDownRight: 0.1, browInnerUp: 0.6, browOuterUpRight: 0.2, eyeSquintLeft: 0.7, eyeSquintRight: 0.7, mouthFrownLeft: 0.8, mouthFrownRight: 0.8, mouthLeft: 0.2, mouthPucker: 0.5, mouthRollLower: 0.2, mouthRollUpper: 0.2, mouthShrugLower: 0.2, mouthShrugUpper: 0.2, mouthStretchLeft: 0.4 },
493
+ speech: { deltaRate: -0.2, deltaPitch: -0.2, deltaVolume: 0 },
494
+ anims: [
495
+ { name: 'breathing', delay: 1500, dt: [ 1000,500,1000 ], vs: { chestInhale: [0.3,0.3,0] } },
496
+ { name: 'pose', alt: [
497
+ { p: 0.4, delay: [5000,30000], vs: { pose: ['side'] } },
498
+ { p: 0.4, delay: [5000,30000], vs: { pose: ['straight'] } },
499
+ { delay: [5000,20000], vs: { pose: ['side'] },
500
+ full: { delay: [5000,20000], vs: { pose: ['oneknee'] } }
501
+ },
502
+ ]},
503
+ { name: 'head',
504
+ idle: { delay: [100,500], dt: [ [200,5000] ], vs: { bodyRotateX: [[-0.04,0.10]], bodyRotateY: [[-0.2,0.2]], bodyRotateZ: [[-0.08,0.08]] } },
505
+ speaking: { dt: [ [0,1000,0] ], vs: { bodyRotateX: [[-0.05,0.15,1,2]], bodyRotateY: [[-0.1,0.1]], bodyRotateZ: [[-0.1,0.1]] } }
506
+ },
507
+ this.animTemplateEyes,
508
+ this.animTemplateBlink,
509
+ { name: 'mouth', delay: [1000,5000], dt: [ [100,500],[100,5000,2] ], vs : { mouthRollLower: [[0,0.3,2]], mouthRollUpper: [[0,0.3,2]], mouthStretchLeft: [[0,0.3]], mouthStretchRight: [[0,0.3]], mouthPucker: [[0,0.3]] } },
510
+ { name: 'misc', delay: [100,5000], dt: [ [100,500],[1000,5000,2] ], vs : { eyeSquintLeft: [[0,0.3,2]], eyeSquintRight: [[0,0.3,2]], browInnerUp: [[0,0.3,2]], browOuterUpLeft: [[0,0.3,2]], browOuterUpRight: [[0,0.3,2]] } }
511
+ ]
512
+ },
513
+ 'fear' : {
514
+ baseline: { browInnerUp: 0.7, eyeSquintLeft: 0.5, eyeSquintRight: 0.5, eyeWideLeft: 0.6, eyeWideRight: 0.6, mouthClose: 0.1, mouthFunnel: 0.3, mouthShrugLower: 0.5, mouthShrugUpper: 0.5 },
515
+ speech: { deltaRate: -0.2, deltaPitch: 0, deltaVolume: 0 },
516
+ anims: [
517
+ { name: 'breathing', delay: 500, dt: [ 1000,500,1000 ], vs: { chestInhale: [0.7,0.7,0] } },
518
+ { name: 'pose', alt: [
519
+ { p: 0.8, delay: [5000,30000], vs: { pose: ['side'] } },
520
+ { delay: [5000,30000], vs: { pose: ['straight'] } },
521
+ { delay: [5000,20000], vs: { pose: ['wide'] } },
522
+ { delay: [5000,20000], vs: { pose: ['side'] },
523
+ full: { delay: [5000,20000], vs: { pose: ['oneknee'] } }
524
+ },
525
+ ]},
526
+ { name: 'head',
527
+ idle: { delay: [100,500], dt: [ [200,3000] ], vs: { bodyRotateX: [[-0.06,0.12]], bodyRotateY: [[-0.7,0.7]], bodyRotateZ: [[-0.1,0.1]] } },
528
+ speaking: { dt: [ [0,1000,0] ], vs: { bodyRotateX: [[-0.05,0.15,1,2]], bodyRotateY: [[-0.1,0.1]], bodyRotateZ: [[-0.1,0.1]] } }
529
+ },
530
+ this.animTemplateEyes,
531
+ this.animTemplateBlink,
532
+ { name: 'mouth', delay: [1000,5000], dt: [ [100,500],[100,5000,2] ], vs : { mouthRollLower: [[0,0.3,2]], mouthRollUpper: [[0,0.3,2]], mouthStretchLeft: [[0,0.3]], mouthStretchRight: [[0,0.3]], mouthPucker: [[0,0.3]] } },
533
+ { name: 'misc', delay: [100,5000], dt: [ [100,500],[1000,5000,2] ], vs : { eyeSquintLeft: [[0,0.3,2]], eyeSquintRight: [[0,0.3,2]], browInnerUp: [[0,0.3,2]], browOuterUpLeft: [[0,0.3,2]], browOuterUpRight: [[0,0.3,2]] } }
534
+ ]
535
+ },
536
+ 'disgust' : {
537
+ baseline: { browDownLeft: 0.7, browDownRight: 0.1, browInnerUp: 0.3, eyeSquintLeft: 1, eyeSquintRight: 1, eyeWideLeft: 0.5, eyeWideRight: 0.5, eyesRotateX: 0.05, mouthLeft: 0.4, mouthPressLeft: 0.3, mouthRollLower: 0.3, mouthShrugLower: 0.3, mouthShrugUpper: 0.8, mouthUpperUpLeft: 0.3, noseSneerLeft: 1, noseSneerRight: 0.7 },
538
+ speech: { deltaRate: -0.2, deltaPitch: 0, deltaVolume: 0 },
539
+ anims: [
540
+ { name: 'breathing', delay: 1500, dt: [ 1000,500,1000 ], vs: { chestInhale: [0.5,0.5,0] } },
541
+ { name: 'pose', alt: [
542
+ { delay: [5000,20000], vs: { pose: ['side'] } },
543
+ ]},
544
+ { name: 'head',
545
+ idle: { delay: [100,500], dt: [ [200,5000] ], vs: { bodyRotateX: [[-0.04,0.10]], bodyRotateY: [[-0.2,0.2]], bodyRotateZ: [[-0.08,0.08]] } },
546
+ speaking: { dt: [ [0,1000,0] ], vs: { bodyRotateX: [[-0.05,0.15,1,2]], bodyRotateY: [[-0.1,0.1]], bodyRotateZ: [[-0.1,0.1]] } }
547
+ },
548
+ this.animTemplateEyes,
549
+ this.animTemplateBlink,
550
+ { name: 'mouth', delay: [1000,5000], dt: [ [100,500],[100,5000,2] ], vs : { mouthRollLower: [[0,0.3,2]], mouthRollUpper: [[0,0.3,2]], mouthStretchLeft: [[0,0.3]], mouthStretchRight: [[0,0.3]], mouthPucker: [[0,0.3]] } },
551
+ { name: 'misc', delay: [100,5000], dt: [ [100,500],[1000,5000,2] ], vs : { eyeSquintLeft: [[0,0.3,2]], eyeSquintRight: [[0,0.3,2]], browInnerUp: [[0,0.3,2]], browOuterUpLeft: [[0,0.3,2]], browOuterUpRight: [[0,0.3,2]] } }
552
+ ]
553
+ },
554
+ 'love' : {
555
+ baseline: { browInnerUp: 0.4, browOuterUpLeft: 0.2, browOuterUpRight: 0.2, mouthSmile: 0.2, eyeBlinkLeft: 0.6, eyeBlinkRight: 0.6, eyeWideLeft: 0.7, eyeWideRight: 0.7, bodyRotateX: 0.1, mouthDimpleLeft: 0.1, mouthDimpleRight: 0.1, mouthPressLeft: 0.2, mouthShrugUpper: 0.2, mouthUpperUpLeft: 0.1, mouthUpperUpRight: 0.1 },
556
+ speech: { deltaRate: -0.1, deltaPitch: -0.7, deltaVolume: 0 },
557
+ anims: [
558
+ { name: 'breathing', delay: 1500, dt: [ 1500,500,1500 ], vs: { chestInhale: [0.8,0.8,0] } },
559
+ { name: 'pose', alt: [
560
+ { p: 0.4, delay: [5000,30000], vs: { pose: ['side'] } },
561
+ { p: 0.2, delay: [5000,30000], vs: { pose: ['straight'] } },
562
+ { p: 0.2, delay: [5000,30000], vs: { pose: ['hip'] },
563
+ 'M': { delay: [5000,30000], vs: { pose: ['side'] } }
564
+ },
565
+ { delay: [5000,10000], vs: { pose: ['side'] },
566
+ full: { delay: [5000,10000], vs: { pose: ['kneel'] } }
567
+ },
568
+ { delay: [1000,3000], vs: { pose: ['turn'] },
569
+ 'M': { delay: [1000,3000], vs: { pose: ['wide'] } }
570
+ },
571
+ { delay: [1000,3000], vs: { pose: ['back'] },
572
+ 'M': { delay: [1000,3000], vs: { pose: ['wide'] } }
573
+ },
574
+ { delay: [5000,20000], vs: { pose: ['side'] },
575
+ 'M': { delay: [5000,20000], vs: { pose: ['side'] } },
576
+ full: { delay: [5000,20000], vs: { pose: ['bend'] } }
577
+ },
578
+ { delay: [1000,3000], vs: { pose: ['side'] },
579
+ full: { delay: [5000,10000], vs: { pose: ['oneknee'] } }
580
+ },
581
+ ]},
582
+ { name: 'head',
583
+ idle: { dt: [ [1000,5000] ], vs: { bodyRotateX: [[-0.04,0.10]], bodyRotateY: [[-0.3,0.3]], bodyRotateZ: [[-0.08,0.08]] } },
584
+ speaking: { dt: [ [0,1000,0] ], vs: { bodyRotateX: [[-0.05,0.15,1,2]], bodyRotateY: [[-0.1,0.1]], bodyRotateZ: [[-0.1,0.1]] } }
585
+ },
586
+ this.animTemplateEyes,
587
+ this.deepCopy(this.animTemplateBlink,(o) => { o.alt[0].delay[0] = o.alt[1].delay[0] = 2000; }),
588
+ { name: 'mouth', delay: [1000,5000], dt: [ [100,500],[100,5000,2] ], vs : { mouthLeft: [[0,0.3,2]], mouthRollLower: [[0,0.3,2]], mouthRollUpper: [[0,0.3,2]], mouthStretchLeft: [[0,0.3]], mouthStretchRight: [[0,0.3]], mouthPucker: [[0,0.3]] } },
589
+ { name: 'misc', delay: [100,5000], dt: [ [500,1000],[1000,5000,2] ], vs : { eyeSquintLeft: [[0,0.3,2]], eyeSquintRight: [[0,0.3,2]], browInnerUp: [[0.3,0.6,2]], browOuterUpLeft: [[0.1,0.3,2]], browOuterUpRight: [[0.1,0.3,2]] } }
590
+ ]
591
+ },
592
+ 'sleep' : {
593
+ baseline: { eyeBlinkLeft: 1, eyeBlinkRight: 1, eyesClosed: 0.6 },
594
+ speech: { deltaRate: 0, deltaPitch: -0.2, deltaVolume: 0 },
595
+ anims: [
596
+ { name: 'breathing', delay: 1500, dt: [ 1000,500,1000 ], vs: { chestInhale: [0.6,0.6,0] } },
597
+ { name: 'pose', alt: [
598
+ { delay: [5000,20000], vs: { pose: ['side'] } }
599
+ ]},
600
+ { name: 'head', delay: [1000,5000], dt: [ [2000,10000] ], vs: { bodyRotateX: [[0,0.4]], bodyRotateY: [[-0.1,0.1]], bodyRotateZ: [[-0.04,0.04]] } },
601
+ { name: 'eyes', delay: 10010, dt: [], vs: {} },
602
+ { name: 'blink', delay: 10020, dt: [], vs: {} },
603
+ { name: 'mouth', delay: 10030, dt: [], vs: {} },
604
+ { name: 'misc', delay: 10040, dt: [], vs: {} }
605
+ ]
606
+ }
607
+ };
608
+ this.moodName = this.opt.avatarMood || "neutral";
609
+ this.mood = this.animMoods[ this.moodName ];
610
+ if ( !this.mood ) {
611
+ this.moodName = "neutral";
612
+ this.mood = this.animMoods["neutral"];
613
+ }
614
+
615
+ // Animation templates for emojis
616
+ this.animEmojis = {
617
+
618
+ '😐': { dt: [300,2000], rescale: [0,1], vs: { pose: ['straight'], browInnerUp: [0.4], eyeWideLeft: [0.7], eyeWideRight: [0.7], mouthPressLeft: [0.6], mouthPressRight: [0.6], mouthRollLower: [0.3], mouthStretchLeft: [1], mouthStretchRight: [1] } },
619
+ '😶': { link: '😐' },
620
+ '😏': { dt: [300,2000], rescale: [0,1], vs: { eyeContact: [0], browDownRight: [0.1], browInnerUp: [0.7], browOuterUpRight: [0.2], eyeLookInRight: [0.7], eyeLookOutLeft: [0.7], eyeSquintLeft: [1], eyeSquintRight: [0.8], eyesRotateY: [0.7], mouthLeft: [0.4], mouthPucker: [0.4], mouthShrugLower: [0.3], mouthShrugUpper: [0.2], mouthSmile: [0.2], mouthSmileLeft: [0.4], mouthSmileRight: [0.2], mouthStretchLeft: [0.5], mouthUpperUpLeft: [0.6], noseSneerLeft: [0.7] } },
621
+ '🙂': { dt: [300,2000], rescale: [0,1], vs: { mouthSmile: [0.5] } },
622
+ '🙃': { link: '🙂' },
623
+ '😊': { dt: [300,2000], rescale: [0,1], vs: { browInnerUp: [0.6], eyeSquintLeft: [1], eyeSquintRight: [1], mouthSmile: [0.7], noseSneerLeft: [0.7], noseSneerRight: [0.7]} },
624
+ '😇': { link: '😊' },
625
+ '😀': { dt: [300,2000], rescale: [0,1], vs: { browInnerUp: [0.6], jawOpen: [0.1], mouthDimpleLeft: [0.2], mouthDimpleRight: [0.2], mouthOpen: [0.3], mouthPressLeft: [0.3], mouthPressRight: [0.3], mouthRollLower: [0.4], mouthShrugUpper: [0.4], mouthSmile: [0.7], mouthUpperUpLeft: [0.3], mouthUpperUpRight: [0.3], noseSneerLeft: [0.4], noseSneerRight: [0.4] }},
626
+ '😃': { dt: [300,2000], rescale: [0,1], vs: { browInnerUp: [0.6], eyeWideLeft: [0.7], eyeWideRight: [0.7], jawOpen: [0.1], mouthDimpleLeft: [0.2], mouthDimpleRight: [0.2], mouthOpen: [0.3], mouthPressLeft: [0.3], mouthPressRight: [0.3], mouthRollLower: [0.4], mouthShrugUpper: [0.4], mouthSmile: [0.7], mouthUpperUpLeft: [0.3], mouthUpperUpRight: [0.3], noseSneerLeft: [0.4], noseSneerRight: [0.4] } },
627
+ '😄': { dt: [300,2000], rescale: [0,1], vs: { browInnerUp: [0.3], eyeSquintLeft: [1], eyeSquintRight: [1], jawOpen: [0.2], mouthDimpleLeft: [0.2], mouthDimpleRight: [0.2], mouthOpen: [0.3], mouthPressLeft: [0.3], mouthPressRight: [0.3], mouthRollLower: [0.4], mouthShrugUpper: [0.4], mouthSmile: [0.7], mouthUpperUpLeft: [0.3], mouthUpperUpRight: [0.3], noseSneerLeft: [0.4], noseSneerRight: [0.4] } },
628
+ '😁': { dt: [300,2000], rescale: [0,1], vs: { browInnerUp: [0.3], eyeSquintLeft: [1], eyeSquintRight: [1], jawOpen: [0.3], mouthDimpleLeft: [0.2], mouthDimpleRight: [0.2], mouthPressLeft: [0.5], mouthPressRight: [0.5], mouthShrugUpper: [0.4], mouthSmile: [0.7], mouthUpperUpLeft: [0.3], mouthUpperUpRight: [0.3], noseSneerLeft: [0.4], noseSneerRight: [0.4] } },
629
+ '😆': { dt: [300,2000], rescale: [0,1], vs: { browInnerUp: [0.3], eyeSquintLeft: [1], eyeSquintRight: [1], eyesClosed: [0.6], jawOpen: [0.3], mouthDimpleLeft: [0.2], mouthDimpleRight: [0.2], mouthPressLeft: [0.5], mouthPressRight: [0.5], mouthShrugUpper: [0.4], mouthSmile: [0.7], mouthUpperUpLeft: [0.3], mouthUpperUpRight: [0.3], noseSneerLeft: [0.4], noseSneerRight: [0.4] } },
630
+ '😝': { dt: [300,100,1500,500,500], rescale: [0,0,1,0,0], vs: { browInnerUp: [0.8], eyesClosed: [1], jawOpen: [0.7], mouthFunnel: [0.5], mouthSmile: [1], tongueOut: [0,1,1,0] } },
631
+ '😋': { link: '😝' }, '😛': { link: '😝' }, '😛': { link: '😝' }, '😜': { link: '😝' }, '🤪': { link: '😝' },
632
+ '😂': { dt: [300,2000], rescale: [0,1], vs: { browInnerUp: [0.3], eyeSquintLeft: [1], eyeSquintRight: [1], eyesClosed: [0.6], jawOpen: [0.3], mouthDimpleLeft: [0.2], mouthDimpleRight: [0.2], mouthPressLeft: [0.5], mouthPressRight: [0.5], mouthShrugUpper: [0.4], mouthSmile: [0.7], mouthUpperUpLeft: [0.3], mouthUpperUpRight: [0.3], noseSneerLeft: [0.4], noseSneerRight: [0.4] } },
633
+ '🤣': { link: '😂' }, '😅': { link: '😂' },
634
+ '😉': { dt: [500,200,500,500], rescale: [0,0,0,1], vs: { mouthSmile: [0.5], mouthOpen: [0.2], mouthSmileLeft: [0,0.5,0], eyeBlinkLeft: [0,0.7,0], eyeBlinkRight: [0,0,0], bodyRotateX: [0.05,0.05,0.05,0], bodyRotateZ: [-0.05,-0.05,-0.05,0], browDownLeft: [0,0.7,0], cheekSquintLeft: [0,0.7,0], eyeSquintLeft: [0,1,0], eyesClosed: [0] } },
635
+
636
+ '😭': { dt: [1000,1000], rescale: [0,1], vs: { browInnerUp: [1], eyeSquintLeft: [1], eyeSquintRight: [1], eyesClosed: [0.1], jawOpen: [0], mouthFrownLeft: [1], mouthFrownRight: [1], mouthOpen: [0.5], mouthPucker: [0.5], mouthUpperUpLeft: [0.6], mouthUpperUpRight: [0.6] } },
637
+ '🥺': { dt: [1000,1000], rescale: [0,1], vs: { browDownLeft: [0.2], browDownRight: [0.2], browInnerUp: [1], eyeWideLeft: [0.9], eyeWideRight: [0.9], eyesClosed: [0.1], mouthClose: [0.2], mouthFrownLeft: [1], mouthFrownRight: [1], mouthPressLeft: [0.4], mouthPressRight: [0.4], mouthPucker: [1], mouthRollLower: [0.6], mouthRollUpper: [0.2], mouthUpperUpLeft: [0.8], mouthUpperUpRight: [0.8] } },
638
+ '😞': { dt: [1000,1000], rescale: [0,1], vs: { browInnerUp: [0.7], eyeSquintLeft: [1], eyeSquintRight: [1], eyesClosed: [0.5], bodyRotateX: [0.3], mouthClose: [0.2], mouthFrownLeft: [1], mouthFrownRight: [1], mouthPucker: [1], mouthRollLower: [1], mouthShrugLower: [0.2], mouthUpperUpLeft: [0.8], mouthUpperUpRight: [0.8] } },
639
+ '😔': { dt: [1000,1000], rescale: [0,1], vs: { browInnerUp: [1], eyeSquintLeft: [1], eyeSquintRight: [1], eyesClosed: [0.5], bodyRotateX: [0.3], mouthClose: [0.2], mouthFrownLeft: [1], mouthFrownRight: [1], mouthPressLeft: [0.4], mouthPressRight: [0.4], mouthPucker: [1], mouthRollLower: [0.6], mouthRollUpper: [0.2], mouthUpperUpLeft: [0.8], mouthUpperUpRight: [0.8] } },
640
+ '😳': { dt: [1000,1000], rescale: [0,1], vs: { browInnerUp: [1], eyeWideLeft: [0.5], eyeWideRight: [0.5], eyesRotateY: [0.05], eyesRotateX: [0.05], mouthClose: [0.2], mouthFunnel: [0.5], mouthPucker: [0.4], mouthRollLower: [0.4], mouthRollUpper: [0.4] } },
641
+ '☹️': { dt: [500,1500], rescale: [0,1], vs: { mouthFrownLeft: [1], mouthFrownRight: [1], mouthPucker: [0.1], mouthRollLower: [0.8] } },
642
+
643
+ '😚': { dt: [500,1000,1000], rescale: [0,1,0], vs: { browInnerUp: [0.6], eyeBlinkLeft: [1], eyeBlinkRight: [1], eyeSquintLeft: [1], eyeSquintRight: [1], mouthPucker: [0,0.5], noseSneerLeft: [0,0.7], noseSneerRight: [0,0.7], viseme_U: [0,1] } },
644
+ '😘': { dt: [500,500,200,500], rescale: [0,0,0,1], vs: { browInnerUp: [0.6], eyeBlinkLeft: [0,0,1,0], eyeBlinkRight: [0], eyesRotateY: [0], bodyRotateY: [0], bodyRotateX: [0,0.05,0.05,0], bodyRotateZ: [0,-0.05,-0.05,0], eyeSquintLeft: [1], eyeSquintRight: [1], mouthPucker: [0,0.5,0], noseSneerLeft: [0,0.7], noseSneerRight: [0.7], viseme_U: [0,1] } },
645
+ '🥰': { dt: [1000,1000], rescale: [0,1], vs: { browInnerUp: [0.6], eyeSquintLeft: [1], eyeSquintRight: [1], mouthSmile: [0.7], noseSneerLeft: [0.7], noseSneerRight: [0.7] } },
646
+ '😍': { dt: [1000,1000], rescale: [0,1], vs: { browInnerUp: [0.6], jawOpen: [0.1], mouthDimpleLeft: [0.2], mouthDimpleRight: [0.2], mouthOpen: [0.3], mouthPressLeft: [0.3], mouthPressRight: [0.3], mouthRollLower: [0.4], mouthShrugUpper: [0.4], mouthSmile: [0.7], mouthUpperUpLeft: [0.3], mouthUpperUpRight: [0.3], noseSneerLeft: [0.4], noseSneerRight: [0.4] } },
647
+ '🤩': { link: '😍' },
648
+
649
+ '😡': { dt: [1000,1500], rescale: [0,1], vs: { browDownLeft: [1], browDownRight: [1], eyesLookUp: [0.2], jawForward: [0.3], mouthFrownLeft: [1], mouthFrownRight: [1], bodyRotateX: [0.15] } },
650
+ '😠': { dt: [1000,1500], rescale: [0,1], vs: { browDownLeft: [1], browDownRight: [1], eyesLookUp: [0.2], jawForward: [0.3], mouthFrownLeft: [1], mouthFrownRight: [1], bodyRotateX: [0.15] } },
651
+ '🤬': { link: '😠' },
652
+ '😒': { dt: [1000,1000], rescale: [0,1], vs: { eyeContact: [0], browDownRight: [0.1], browInnerUp: [0.7], browOuterUpRight: [0.2], eyeLookInRight: [0.7], eyeLookOutLeft: [0.7], eyeSquintLeft: [1], eyeSquintRight: [0.8], eyesRotateY: [0.7], mouthFrownLeft: [1], mouthFrownRight: [1], mouthLeft: [0.2], mouthPucker: [0.5], mouthRollLower: [0.2], mouthRollUpper: [0.2], mouthShrugLower: [0.2], mouthShrugUpper: [0.2], mouthStretchLeft: [0.5] } },
653
+
654
+ '😱': { dt: [500,1500], rescale: [0,1], vs: { browInnerUp: [0.8], eyeWideLeft: [0.5], eyeWideRight: [0.5], jawOpen: [0.7], mouthFunnel: [0.5] } },
655
+ '😬': { dt: [500,1500], rescale: [0,1], vs: { browDownLeft: [1], browDownRight: [1], browInnerUp: [1], mouthDimpleLeft: [0.5], mouthDimpleRight: [0.5], mouthLowerDownLeft: [1], mouthLowerDownRight: [1], mouthPressLeft: [0.4], mouthPressRight: [0.4], mouthPucker: [0.5], mouthSmile: [0.1], mouthSmileLeft: [0.2], mouthSmileRight: [0.2], mouthStretchLeft: [1], mouthStretchRight: [1], mouthUpperUpLeft: [1], mouthUpperUpRight: [1] } },
656
+ '🙄': { dt: [500,1500], rescale: [0,1], vs: { browInnerUp: [0.8], eyeWideLeft: [1], eyeWideRight: [1], eyesRotateX: [-0.8], bodyRotateX: [0.15], mouthPucker: [0.5], mouthRollLower: [0.6], mouthRollUpper: [0.5], mouthShrugLower: [0], mouthSmile: [0] } },
657
+ '🤔': { dt: [500,1500], rescale: [0,1], vs: {
658
+ browDownLeft: [1], browOuterUpRight: [1], eyeSquintLeft: [0.6],
659
+ mouthFrownLeft: [0.7], mouthFrownRight: [0.7], mouthLowerDownLeft: [0.3],
660
+ mouthPressRight: [0.4], mouthPucker: [0.1], mouthRight: [0.5], mouthRollLower: [0.5],
661
+ mouthRollUpper: [0.2], handRight: [{ x: 0.1, y: 0.1, z:0.1, d:1000 }, { d:1000 }],
662
+ handFistRight: [0.1]
663
+ } },
664
+ '👀': { dt: [500,1500], rescale: [0,1], vs: { eyesRotateY: [-0.8] } },
665
+
666
+ '😴': { dt: [5000,5000], rescale: [0,1], vs:{ eyeBlinkLeft: [1], eyeBlinkRight: [1], bodyRotateX: [0.2], bodyRotateZ: [0.1] } },
667
+
668
+ '✋': { dt: [300,2000], rescale: [0,1], vs:{ mouthSmile: [0.5], gesture: [["handup",2,true],null] } },
669
+ '🤚': { dt: [300,2000], rescale: [0,1], vs:{ mouthSmile: [0.5], gesture: [["handup",2],null] } },
670
+ '👋': { link: '✋' },
671
+ '👍': { dt: [300,2000], rescale: [0,1], vs:{ mouthSmile: [0.5], gesture: [["thumbup",2],null] } },
672
+ '👎': { dt: [300,2000], rescale: [0,1], vs:{ browDownLeft: [1], browDownRight: [1], eyesLookUp: [0.2], jawForward: [0.3], mouthFrownLeft: [1], mouthFrownRight: [1], bodyRotateX: [0.15], gesture: [["thumbdown",2],null] } },
673
+ '👌': { dt: [300,2000], rescale: [0,1], vs:{ mouthSmile: [0.5], gesture: [["ok",2],null] } },
674
+ '🤷‍♂️': { dt: [1000,1500], rescale: [0,1], vs:{ gesture: [["shrug",2],null] } },
675
+ '🤷‍♀️': { link: '🤷‍♂️' },
676
+ '🤷': { link: '🤷‍♂️' },
677
+ '🙏': { dt: [1500,300,1000], rescale: [0,1,0], vs:{ eyeBlinkLeft: [0,1], eyeBlinkRight: [0,1], bodyRotateX: [0], bodyRotateZ: [0.1], gesture: [["namaste",2],null] } },
678
+
679
+ 'yes': { dt: [[200,500],[200,500],[200,500],[200,500]], vs:{ headMove: [0], headRotateX: [[0.1,0.2],0.1,[0.1,0.2],0], headRotateZ: [[-0.2,0.2]] } },
680
+ 'no': { dt: [[200,500],[200,500],[200,500],[200,500],[200,500]], vs:{ headMove: [0], headRotateY: [[-0.1,-0.05],[0.05,0.1],[-0.1,-0.05],[0.05,0.1],0], headRotateZ: [[-0.2,0.2]] } }
681
+
682
+ };
683
+
684
+ // Morph targets
685
+ this.mtAvatar = {};
686
+ this.mtCustoms = [
687
+ "handFistLeft","handFistRight",'bodyRotateX', 'bodyRotateY',
688
+ 'bodyRotateZ', 'headRotateX', 'headRotateY', 'headRotateZ','chestInhale'
689
+ ];
690
+ this.mtEasingDefault = this.sigmoidFactory(5); // Morph target default ease in/out
691
+ this.mtAccDefault = 0.01; // Acceleration [rad / s^2]
692
+ this.mtAccExceptions = {
693
+ eyeBlinkLeft: 0.1, eyeBlinkRight: 0.1, eyeLookOutLeft: 0.1,
694
+ eyeLookInLeft: 0.1, eyeLookOutRight: 0.1, eyeLookInRight: 0.1
695
+ };
696
+ this.mtMaxVDefault = 5; // Maximum velocity [rad / s]
697
+ this.mtMaxVExceptions = {
698
+ bodyRotateX: 1, bodyRotateY: 1, bodyRotateZ: 1,
699
+ // headRotateX: 1, headRotateY: 1, headRotateZ: 1
700
+ };
701
+ this.mtBaselineDefault = 0; // Default baseline value
702
+ this.mtBaselineExceptions = {
703
+ bodyRotateX: null, bodyRotateY: null, bodyRotateZ: null,
704
+ eyeLookOutLeft: null, eyeLookInLeft: null, eyeLookOutRight: null,
705
+ eyeLookInRight: null, eyesLookDown: null, eyesLookUp: null
706
+ };
707
+ this.mtMinDefault = 0;
708
+ this.mtMinExceptions = {
709
+ bodyRotateX: -1, bodyRotateY: -1, bodyRotateZ: -1,
710
+ headRotateX: -1, headRotateY: -1, headRotateZ: -1
711
+ };
712
+ this.mtMaxDefault = 1;
713
+ this.mtMaxExceptions = {};
714
+ this.mtLimits = {
715
+ eyeBlinkLeft: (v) => ( Math.max(v, ( this.mtAvatar['eyesLookDown'].value + this.mtAvatar['browDownLeft'].value ) / 2) ),
716
+ eyeBlinkRight: (v) => ( Math.max(v, ( this.mtAvatar['eyesLookDown'].value + this.mtAvatar['browDownRight'].value ) / 2 ) )
717
+ };
718
+ this.mtOnchange = {
719
+ eyesLookDown: () => {
720
+ this.mtAvatar['eyeBlinkLeft'].needsUpdate = true;
721
+ this.mtAvatar['eyeBlinkRight'].needsUpdate = true;
722
+ },
723
+ browDownLeft: () => { this.mtAvatar['eyeBlinkLeft'].needsUpdate = true; },
724
+ browDownRight: () => { this.mtAvatar['eyeBlinkRight'].needsUpdate = true; }
725
+ };
726
+ this.mtRandomized = [
727
+ 'mouthDimpleLeft','mouthDimpleRight', 'mouthLeft', 'mouthPressLeft',
728
+ 'mouthPressRight', 'mouthStretchLeft', 'mouthStretchRight',
729
+ 'mouthShrugLower', 'mouthShrugUpper', 'noseSneerLeft', 'noseSneerRight',
730
+ 'mouthRollLower', 'mouthRollUpper', 'browDownLeft', 'browDownRight',
731
+ 'browOuterUpLeft', 'browOuterUpRight', 'cheekPuff', 'cheekSquintLeft',
732
+ 'cheekSquintRight'
733
+ ];
734
+ this.mtExtras = [ // RPM Extras from ARKit, if missing
735
+ { key: "mouthOpen", mix: { jawOpen: 0.5 } },
736
+ { key: "mouthSmile", mix: { mouthSmileLeft: 0.8, mouthSmileRight: 0.8 } },
737
+ { key: "eyesClosed", mix: { eyeBlinkLeft: 1.0, eyeBlinkRight: 1.0 } },
738
+ { key: "eyesLookUp", mix: { eyeLookUpLeft: 1.0, eyeLookUpRight: 1.0 } },
739
+ { key: "eyesLookDown", mix: { eyeLookDownLeft: 1.0, eyeLookDownRight: 1.0 } }
740
+ ];
741
+
742
+ // Anim queues
743
+ this.animQueue = [];
744
+ this.animClips = [];
745
+ this.animPoses = [];
746
+
747
+ // Animate
748
+ this.animate = this.animate.bind(this);
749
+ this._raf = null;
750
+
751
+ // Clock
752
+ this.animFrameDur = 1000/ this.opt.modelFPS;
753
+ this.animClock = 0;
754
+ this.animSlowdownRate = 1;
755
+ this.animTimeLast = 0;
756
+ this.easing = this.sigmoidFactory(5); // Ease in and out
757
+
758
+ // Lip-sync extensions, import dynamically
759
+ this.lipsync = {};
760
+ this.opt.lipsyncModules.forEach( x => this.lipsyncGetProcessor(x) );
761
+ this.visemeNames = [
762
+ 'aa', 'E', 'I', 'O', 'U', 'PP', 'SS', 'TH', 'DD', 'FF', 'kk',
763
+ 'nn', 'RR', 'CH', 'sil'
764
+ ];
765
+
766
+ // Grapheme segmenter
767
+ this.segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
768
+
769
+ // Audio context and playlist
770
+ this.initAudioGraph();
771
+ this.audioPlaylist = [];
772
+
773
+ // Volume based head movement
774
+ this.volumeFrequencyData = new Uint8Array(16);
775
+ this.volumeMax = 0;
776
+ this.volumeHeadBase = 0;
777
+ this.volumeHeadTarget = 0;
778
+ this.volumeHeadCurrent = 0;
779
+ this.volumeHeadVelocity = 0.15;
780
+ this.volumeHeadEasing = this.sigmoidFactory(3);
781
+
782
+ // Listening
783
+ this.isListening = false;
784
+ this.listeningAnalyzer = null;
785
+ this.listeningActive = false;
786
+ this.listeningVolume = 0;
787
+ this.listeningSilenceThresholdLevel = this.opt.listeningSilenceThresholdLevel;
788
+ this.listeningSilenceThresholdMs = this.opt.listeningSilenceThresholdMs;
789
+ this.listeningSilenceDurationMax = this.opt.listeningSilenceDurationMax;
790
+ this.listeningActiveThresholdLevel = this.opt.listeningActiveThresholdLevel;
791
+ this.listeningActiveThresholdMs = this.opt.listeningActiveThresholdMs;
792
+ this.listeningActiveDurationMax = this.opt.listeningActiveDurationMax;
793
+ this.listeningTimer = 0;
794
+ this.listeningTimerTotal = 0;
795
+
796
+ // Draco loading
797
+ this.dracoEnabled = this.opt.dracoEnabled;
798
+ this.dracoDecoderPath = this.opt.dracoDecoderPath;
799
+
800
+ // Create a lookup table for base64 decoding
801
+ const b64Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
802
+ this.b64Lookup = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256);
803
+ for (let i = 0; i < b64Chars.length; i++) this.b64Lookup[b64Chars.charCodeAt(i)] = i;
804
+
805
+ // Speech queue
806
+ this.stateName = 'idle';
807
+ this.speechQueue = [];
808
+ this.isSpeaking = false;
809
+ this.isListening = false;
810
+
811
+ // Setup Google text-to-speech
812
+ if ( this.opt.ttsEndpoint ) {
813
+ let audio = new Audio();
814
+ if (audio.canPlayType("audio/ogg")) {
815
+ this.ttsAudioEncoding = "OGG-OPUS";
816
+ } else if (audio.canPlayType("audio/mp3")) {
817
+ this.ttsAudioEncoding = "MP3";
818
+ } else {
819
+ throw new Error("There was no support for either OGG or MP3 audio.");
820
+ }
821
+ }
822
+
823
+ // Avatar only mode
824
+ this.isAvatarOnly = this.opt.avatarOnly;
825
+
826
+ // Setup 3D Animation
827
+ if ( this.isAvatarOnly ) {
828
+ this.scene = this.opt.avatarOnlyScene;
829
+ this.camera = this.opt.avatarOnlyCamera;
830
+ } else {
831
+ this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
832
+ this.renderer.setPixelRatio( this.opt.modelPixelRatio * window.devicePixelRatio );
833
+ this.renderer.setSize(this.nodeAvatar.clientWidth, this.nodeAvatar.clientHeight);
834
+ this.renderer.outputColorSpace = THREE.SRGBColorSpace;
835
+ this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
836
+ this.renderer.shadowMap.enabled = false;
837
+ this.nodeAvatar.appendChild( this.renderer.domElement );
838
+ this.camera = new THREE.PerspectiveCamera( 10, this.nodeAvatar.clientWidth / this.nodeAvatar.clientHeight, 0.1, 2000 );
839
+ this.scene = new THREE.Scene();
840
+ this.lightAmbient = new THREE.AmbientLight(
841
+ new THREE.Color( this.opt.lightAmbientColor ),
842
+ this.opt.lightAmbientIntensity
843
+ );
844
+ this.lightDirect = new THREE.DirectionalLight(
845
+ new THREE.Color( this.opt.lightDirectColor ),
846
+ this.opt.lightDirectIntensity
847
+ );
848
+ this.lightSpot = new THREE.SpotLight(
849
+ new THREE.Color( this.opt.lightSpotColor ),
850
+ this.opt.lightSpotIntensity,
851
+ 0,
852
+ this.opt.lightSpotDispersion
853
+ );
854
+ this.setLighting( this.opt );
855
+ const pmremGenerator = new THREE.PMREMGenerator( this.renderer );
856
+ pmremGenerator.compileEquirectangularShader();
857
+ this.scene.environment = pmremGenerator.fromScene( new RoomEnvironment() ).texture;
858
+ this.resizeobserver = new ResizeObserver(this.onResize.bind(this));
859
+ this.resizeobserver.observe(this.nodeAvatar);
860
+
861
+ this.controls = new OrbitControls( this.camera, this.renderer.domElement );
862
+ this.controls.enableZoom = this.opt.cameraZoomEnable;
863
+ this.controls.enableRotate = this.opt.cameraRotateEnable;
864
+ this.controls.enablePan = this.opt.cameraPanEnable;
865
+ this.controls.minDistance = 2;
866
+ this.controls.maxDistance = 2000;
867
+ this.controls.autoRotateSpeed = 0;
868
+ this.controls.autoRotate = false;
869
+ this.controls.update();
870
+ this.cameraClock = null;
871
+ }
872
+
873
+ // IK Mesh
874
+ this.ikMesh = new THREE.SkinnedMesh();
875
+ const ikSetup = {
876
+ 'LeftShoulder': null, 'LeftArm': 'LeftShoulder', 'LeftForeArm': 'LeftArm',
877
+ 'LeftHand': 'LeftForeArm', 'LeftHandMiddle1': 'LeftHand',
878
+ 'RightShoulder': null, 'RightArm': 'RightShoulder', 'RightForeArm': 'RightArm',
879
+ 'RightHand': 'RightForeArm', 'RightHandMiddle1': 'RightHand'
880
+ };
881
+ const ikBones = [];
882
+ Object.entries(ikSetup).forEach( (x,i) => {
883
+ const bone = new THREE.Bone();
884
+ bone.name = x[0];
885
+ if ( x[1] ) {
886
+ this.ikMesh.getObjectByName(x[1]).add(bone);
887
+ } else {
888
+ this.ikMesh.add(bone);
889
+ }
890
+ ikBones.push(bone);
891
+ });
892
+ this.ikMesh.bind( new THREE.Skeleton( ikBones ) );
893
+
894
+ // Dynamic Bones
895
+ this.dynamicbones = new DynamicBones();
896
+
897
+ // Stream speech mode
898
+ this.isStreaming = false;
899
+ this.streamWorkletNode = null;
900
+ this.streamAudioStartTime = null;
901
+ this.streamWaitForAudioChunks = true;
902
+ this.streamLipsyncLang = null;
903
+ this.streamLipsyncType = "visemes";
904
+ this.streamLipsyncQueue = [];
905
+ }
906
+
907
+ /**
908
+ * Helper that re/creates the audio context and the other nodes.
909
+ * @param {number} sampleRate
910
+ */
911
+ initAudioGraph(sampleRate = null) {
912
+ // Close existing context if it exists
913
+ if (this.audioCtx && this.audioCtx.state !== 'closed') {
914
+ this.audioCtx.close();
915
+ }
916
+
917
+ // Create a new context
918
+ if (sampleRate) {
919
+ this.audioCtx = new AudioContext({ sampleRate });
920
+ } else {
921
+ this.audioCtx = new AudioContext();
922
+ }
923
+
924
+ // Create audio nodes
925
+ this.audioSpeechSource = this.audioCtx.createBufferSource();
926
+ this.audioBackgroundSource = this.audioCtx.createBufferSource();
927
+ this.audioBackgroundGainNode = this.audioCtx.createGain();
928
+ this.audioSpeechGainNode = this.audioCtx.createGain();
929
+ this.audioStreamGainNode = this.audioCtx.createGain();
930
+ this.audioAnalyzerNode = this.audioCtx.createAnalyser();
931
+ this.audioAnalyzerNode.fftSize = 256;
932
+ this.audioAnalyzerNode.smoothingTimeConstant = 0.1;
933
+ this.audioAnalyzerNode.minDecibels = -70;
934
+ this.audioAnalyzerNode.maxDecibels = -10;
935
+ this.audioReverbNode = this.audioCtx.createConvolver();
936
+
937
+ // Connect nodes
938
+ this.audioBackgroundGainNode.connect(this.audioReverbNode);
939
+ this.audioAnalyzerNode.connect(this.audioSpeechGainNode);
940
+ this.audioSpeechGainNode.connect(this.audioReverbNode);
941
+ this.audioStreamGainNode.connect(this.audioReverbNode);
942
+ this.audioReverbNode.connect(this.audioCtx.destination);
943
+
944
+ // Apply reverb and mixer settings
945
+ this.setReverb(this.currentReverb || null);
946
+ this.setMixerGain(
947
+ this.opt.mixerGainSpeech,
948
+ this.opt.mixerGainBackground
949
+ );
950
+
951
+ // Delete the stream audio worklet if initialised
952
+ this.workletLoaded = false;
953
+ if (this.streamWorkletNode) {
954
+ try {
955
+ this.streamWorkletNode.port.postMessage({type: 'stop'});
956
+ this.streamWorkletNode.disconnect();
957
+ this.isStreaming = false;
958
+ } catch(e) {
959
+ console.error('Error disconnecting streamWorkletNode:', e);
960
+ /* ignore */
961
+ }
962
+ this.streamWorkletNode = null;
963
+ }
964
+ }
965
+
966
+ /**
967
+ * Helper that returns the parameter or, if it is a function, its return value.
968
+ * @param {Any} x Parameter
969
+ * @return {Any} Value
970
+ */
971
+ valueFn(x) {
972
+ return (typeof x === 'function' ? x() : x);
973
+ }
974
+
975
+ /**
976
+ * Helper to deep copy and edit an object.
977
+ * @param {Object} x Object to copy and edit
978
+ * @param {function} [editFn=null] Callback function for editing the new object
979
+ * @return {Object} Deep copy of the object.
980
+ */
981
+ deepCopy(x, editFn=null) {
982
+ const o = JSON.parse(JSON.stringify(x));
983
+ if ( editFn && typeof editFn === "function" ) editFn(o);
984
+ return o;
985
+ }
986
+
987
+ /**
988
+ * Convert a Base64 MP3 chunk to ArrayBuffer.
989
+ * @param {string} chunk Base64 encoded chunk
990
+ * @return {ArrayBuffer} ArrayBuffer
991
+ */
992
+ b64ToArrayBuffer(chunk) {
993
+
994
+ // Calculate the needed total buffer length
995
+ let bufLen = 3 * chunk.length / 4;
996
+ if (chunk[chunk.length - 1] === '=') {
997
+ bufLen--;
998
+ if (chunk[chunk.length - 2] === '=') {
999
+ bufLen--;
1000
+ }
1001
+ }
1002
+
1003
+ // Create the ArrayBuffer
1004
+ const arrBuf = new ArrayBuffer(bufLen);
1005
+ const arr = new Uint8Array(arrBuf);
1006
+ let i, p = 0, c1, c2, c3, c4;
1007
+
1008
+ // Populate the buffer
1009
+ for (i = 0; i < chunk.length; i += 4) {
1010
+ c1 = this.b64Lookup[chunk.charCodeAt(i)];
1011
+ c2 = this.b64Lookup[chunk.charCodeAt(i+1)];
1012
+ c3 = this.b64Lookup[chunk.charCodeAt(i+2)];
1013
+ c4 = this.b64Lookup[chunk.charCodeAt(i+3)];
1014
+ arr[p++] = (c1 << 2) | (c2 >> 4);
1015
+ arr[p++] = ((c2 & 15) << 4) | (c3 >> 2);
1016
+ arr[p++] = ((c3 & 3) << 6) | (c4 & 63);
1017
+ }
1018
+
1019
+ return arrBuf;
1020
+ }
1021
+
1022
+ /**
1023
+ * Concatenate an array of ArrayBuffers.
1024
+ * @param {ArrayBuffer[]} bufs Array of ArrayBuffers
1025
+ * @return {ArrayBuffer} Concatenated ArrayBuffer
1026
+ */
1027
+ concatArrayBuffers(bufs) {
1028
+ if ( bufs.length === 1 ) return bufs[0];
1029
+ let len = 0;
1030
+ for( let i=0; i<bufs.length; i++ ) {
1031
+ len += bufs[i].byteLength;
1032
+ }
1033
+ let buf = new ArrayBuffer(len);
1034
+ let arr = new Uint8Array(buf);
1035
+ let p = 0;
1036
+ for( let i=0; i<bufs.length; i++ ) {
1037
+ arr.set( new Uint8Array(bufs[i]), p);
1038
+ p += bufs[i].byteLength;
1039
+ }
1040
+ return buf;
1041
+ }
1042
+
1043
+
1044
+ /**
1045
+ * Convert PCM buffer to AudioBuffer.
1046
+ * NOTE: Only signed 16bit little endian supported.
1047
+ * @param {ArrayBuffer} buf PCM buffer
1048
+ * @return {AudioBuffer} AudioBuffer
1049
+ */
1050
+ pcmToAudioBuffer(buf) {
1051
+ const arr = new Int16Array(buf);
1052
+ const floats = new Float32Array(arr.length);
1053
+ for( let i=0; i<arr.length; i++ ) {
1054
+ floats[i] = (arr[i] >= 0x8000) ? -(0x10000 - arr[i]) / 0x8000 : arr[i] / 0x7FFF;
1055
+ }
1056
+ const audio = this.audioCtx.createBuffer(1, floats.length, this.opt.pcmSampleRate );
1057
+ audio.copyToChannel( floats, 0 , 0 );
1058
+ return audio;
1059
+ }
1060
+
1061
+
1062
+ /**
1063
+ * Convert internal notation to THREE objects.
1064
+ * NOTE: All rotations are converted to quaternions.
1065
+ * @param {Object} p Pose
1066
+ * @return {Object} A new pose object.
1067
+ */
1068
+ propsToThreeObjects(p) {
1069
+ const r = {};
1070
+ for( let [key,val] of Object.entries(p) ) {
1071
+ const ids = key.split('.');
1072
+ let x = Array.isArray(val.x) ? this.gaussianRandom(...val.x) : val.x;
1073
+ let y = Array.isArray(val.y) ? this.gaussianRandom(...val.y) : val.y;
1074
+ let z = Array.isArray(val.z) ? this.gaussianRandom(...val.z) : val.z;
1075
+
1076
+ if ( ids[1] === 'position' || ids[1] === 'scale' ) {
1077
+ r[key] = new THREE.Vector3(x,y,z);
1078
+ } else if ( ids[1] === 'rotation' ) {
1079
+ key = ids[0] + '.quaternion';
1080
+ r[key] = new THREE.Quaternion().setFromEuler(new THREE.Euler(x,y,z,'XYZ')).normalize();
1081
+ } else if ( ids[1] === 'quaternion' ) {
1082
+ r[key] = new THREE.Quaternion(x,y,z,val.w).normalize();
1083
+ }
1084
+ }
1085
+
1086
+ return r;
1087
+ }
1088
+
1089
+
1090
+ /**
1091
+ * Clear 3D object.
1092
+ * @param {Object} obj Object
1093
+ */
1094
+ clearThree(obj) {
1095
+ while (obj.children.length) {
1096
+ this.clearThree(obj.children[0]);
1097
+ obj.remove(obj.children[0]);
1098
+ }
1099
+
1100
+ if (obj.geometry) obj.geometry.dispose();
1101
+
1102
+ if (obj.material) {
1103
+ if (Array.isArray(obj.material)) {
1104
+ obj.material.forEach(m => {
1105
+ if (m.map) m.map.dispose();
1106
+ m.dispose();
1107
+ });
1108
+ } else {
1109
+ if (obj.material.map) obj.material.map.dispose();
1110
+ obj.material.dispose();
1111
+ }
1112
+ }
1113
+ }
1114
+
1115
+ /**
1116
+ * Adds a new mixed morph target based on the given sources.
1117
+ * Note: This assumes that morphTargetsRelative === true (default for GLTF)
1118
+ *
1119
+ * @param {Object[]} meshes Meshes to process
1120
+ * @param {string} name New of the new morph target (a.k.a. shape key)
1121
+ * @param {Object} sources Object of existing morph target values, e.g. { mouthOpen: 1.0 }
1122
+ * @param {boolean} [override=false] If true, override existing morph target
1123
+ */
1124
+ addMixedMorphTarget(meshes, name, sources, override=false ) {
1125
+
1126
+ meshes.forEach( x => {
1127
+
1128
+ // Skip, we already have a morph target with the same name and we do not override
1129
+ if ( !override && x.morphTargetDictionary.hasOwnProperty(name) ) return;
1130
+
1131
+ // Check if this mesh has any sources to add to the mix
1132
+ const g = x.geometry;
1133
+ let mixPos = null;
1134
+ let mixNor = null;
1135
+ for( const [k,v] of Object.entries(sources) ) {
1136
+ if ( x.morphTargetDictionary.hasOwnProperty(k) ) {
1137
+ const index = x.morphTargetDictionary[k];
1138
+ const pos = g.morphAttributes.position[index];
1139
+ const nor = g.morphAttributes.normal?.[index];
1140
+
1141
+ // Create position and normal
1142
+ if ( !mixPos ) {
1143
+ mixPos = new THREE.Float32BufferAttribute(pos.count * 3, 3);
1144
+ if ( nor ) {
1145
+ mixNor = new THREE.Float32BufferAttribute(pos.count * 3, 3);
1146
+ }
1147
+ }
1148
+
1149
+ // Update position
1150
+ for (let i = 0; i < pos.count; i++) {
1151
+ const dx = mixPos.getX(i) + pos.getX(i) * v;
1152
+ const dy = mixPos.getY(i) + pos.getY(i) * v;
1153
+ const dz = mixPos.getZ(i) + pos.getZ(i) * v;
1154
+ mixPos.setXYZ(i, dx, dy, dz);
1155
+ }
1156
+
1157
+ // Update normal
1158
+ if ( nor ) {
1159
+ for (let i = 0; i < pos.count; i++) {
1160
+ const dx = mixNor.getX(i) + nor.getX(i) * v;
1161
+ const dy = mixNor.getY(i) + nor.getY(i) * v;
1162
+ const dz = mixNor.getZ(i) + nor.getZ(i) * v;
1163
+ mixNor.setXYZ(i, dx, dy, dz);
1164
+ }
1165
+ }
1166
+
1167
+ }
1168
+ }
1169
+
1170
+ // We found one or more sources, so we add the new mixed morph target
1171
+ if ( mixPos ) {
1172
+ g.morphAttributes.position.push(mixPos);
1173
+ if ( mixNor ) {
1174
+ g.morphAttributes.normal.push(mixNor);
1175
+ }
1176
+ const index = g.morphAttributes.position.length - 1;
1177
+ x.morphTargetInfluences[index] = 0;
1178
+ x.morphTargetDictionary[name] = index;
1179
+ }
1180
+
1181
+ });
1182
+ }
1183
+
1184
+ /**
1185
+ * Loader for 3D avatar model.
1186
+ * @param {string} avatar Avatar object with 'url' property to GLTF/GLB file.
1187
+ * @param {progressfn} [onprogress=null] Callback for progress
1188
+ */
1189
+ async showAvatar(avatar, onprogress=null ) {
1190
+
1191
+ // Checkt the avatar parameter
1192
+ if ( !avatar || !avatar.hasOwnProperty('url') ) {
1193
+ throw new Error("Invalid parameter. The avatar must have at least 'url' specified.");
1194
+ }
1195
+
1196
+ // Loader
1197
+ const loader = new GLTFLoader();
1198
+
1199
+ // Check if draco loading enabled
1200
+ if ( this.dracoEnabled ) {
1201
+ const dracoLoader = new DRACOLoader();
1202
+ dracoLoader.setDecoderPath( this.dracoDecoderPath );
1203
+ loader.setDRACOLoader( dracoLoader );
1204
+ }
1205
+
1206
+ let gltf = await loader.loadAsync( avatar.url, onprogress );
1207
+
1208
+ // Check the gltf
1209
+ const required = [ this.opt.modelRoot ];
1210
+ this.posePropNames.forEach( x => required.push( x.split('.')[0] ) );
1211
+ required.forEach( x => {
1212
+ if ( !gltf.scene.getObjectByName(x) ) {
1213
+ throw new Error('Avatar object ' + x + ' not found');
1214
+ }
1215
+ });
1216
+
1217
+ this.stop();
1218
+ this.avatar = avatar;
1219
+
1220
+ // Dispose Dynamic Bones
1221
+ this.dynamicbones.dispose();
1222
+
1223
+ // Clear previous mixer/scene, if avatar was previously loaded
1224
+ if (this.mixer) {
1225
+ this.mixer.removeEventListener('finished', this._mixerHandler);
1226
+ this.mixer.stopAllAction();
1227
+ this.mixer.uncacheRoot(this.armature);
1228
+ this.mixer = null;
1229
+ this._mixerHandler = null;
1230
+ }
1231
+ if ( this.isAvatarOnly ) {
1232
+ if ( this.armature ) {
1233
+ this.clearThree( this.armature );
1234
+ }
1235
+ } else {
1236
+ if ( this.armature ) {
1237
+ this.clearThree( this.scene );
1238
+ }
1239
+ }
1240
+
1241
+ // Avatar full-body
1242
+ this.armature = gltf.scene.getObjectByName( this.opt.modelRoot );
1243
+ this.armature.scale.setScalar(1);
1244
+
1245
+ // Expose GLB animations and userData
1246
+ this.animations = gltf.animations;
1247
+ this.userData = gltf.userData;
1248
+
1249
+ // Morph targets
1250
+ this.morphs = [];
1251
+ this.armature.traverse( x => {
1252
+ if ( x.morphTargetInfluences && x.morphTargetInfluences.length &&
1253
+ x.morphTargetDictionary ) {
1254
+ this.morphs.push(x);
1255
+ }
1256
+
1257
+ // Workaround for #40, hands culled from the rendering process
1258
+ x.frustumCulled = false;
1259
+ });
1260
+ if ( this.morphs.length === 0 ) {
1261
+ throw new Error('Blend shapes not found');
1262
+ }
1263
+
1264
+ // Morph target keys and values
1265
+ const keys = new Set(this.mtCustoms);
1266
+ this.morphs.forEach( x => {
1267
+ Object.keys(x.morphTargetDictionary).forEach( y => keys.add(y) );
1268
+ });
1269
+
1270
+ // Add RPM extra blend shapes, if missing
1271
+ this.mtExtras.forEach( x => {
1272
+ if ( !keys.has(x.key) ) {
1273
+ this.addMixedMorphTarget( this.morphs, x.key, x.mix );
1274
+ keys.add(x.key);
1275
+ }
1276
+ });
1277
+
1278
+ // Create internal morph target data structure
1279
+ const mtTemp = {};
1280
+ keys.forEach( x => {
1281
+
1282
+ // Morph target data structure
1283
+ mtTemp[x] = {
1284
+ fixed: null, realtime: null, system: null, systemd: null, newvalue: null, ref: null,
1285
+ min: (this.mtMinExceptions.hasOwnProperty(x) ? this.mtMinExceptions[x] : this.mtMinDefault),
1286
+ max: (this.mtMaxExceptions.hasOwnProperty(x) ? this.mtMaxExceptions[x] : this.mtMaxDefault),
1287
+ easing: this.mtEasingDefault, base: null, v: 0, needsUpdate: true,
1288
+ acc: (this.mtAccExceptions.hasOwnProperty(x) ? this.mtAccExceptions[x] : this.mtAccDefault) / 1000,
1289
+ maxv: (this.mtMaxVExceptions.hasOwnProperty(x) ? this.mtMaxVExceptions[x] : this.mtMaxVDefault) / 1000,
1290
+ limit: this.mtLimits.hasOwnProperty(x) ? this.mtLimits[x] : null,
1291
+ onchange: this.mtOnchange.hasOwnProperty(x) ? this.mtOnchange[x] : null,
1292
+ baseline: this.avatar.baseline?.hasOwnProperty(x) ? this.avatar.baseline[x] : (this.mtBaselineExceptions.hasOwnProperty(x) ? this.mtBaselineExceptions[x] : this.mtBaselineDefault ),
1293
+ ms: [], is: []
1294
+ };
1295
+ mtTemp[x].value = mtTemp[x].baseline;
1296
+ mtTemp[x].applied = mtTemp[x].baseline;
1297
+
1298
+ // Copy previous values
1299
+ const y = this.mtAvatar[x];
1300
+ if ( y ) {
1301
+ [ 'fixed','system','systemd','realtime','base','v','value','applied' ].forEach( z => {
1302
+ mtTemp[x][z] = y[z];
1303
+ });
1304
+ }
1305
+
1306
+ // Find relevant meshes
1307
+ this.morphs.forEach( y => {
1308
+ const ndx = y.morphTargetDictionary[x];
1309
+ if ( ndx !== undefined ) {
1310
+ mtTemp[x].ms.push(y.morphTargetInfluences);
1311
+ mtTemp[x].is.push(ndx);
1312
+ y.morphTargetInfluences[ndx] = mtTemp[x].applied;
1313
+ }
1314
+ });
1315
+
1316
+ });
1317
+ this.mtAvatar = mtTemp;
1318
+
1319
+ // Objects for needed properties
1320
+ this.poseAvatar = { props: {} };
1321
+ this.posePropNames.forEach( x => {
1322
+ const ids = x.split('.');
1323
+ const o = this.armature.getObjectByName(ids[0]);
1324
+ this.poseAvatar.props[x] = o[ids[1]];
1325
+ if ( this.poseBase.props.hasOwnProperty(x) ) {
1326
+ this.poseAvatar.props[x].copy( this.poseBase.props[x] );
1327
+ } else {
1328
+ this.poseBase.props[x] = this.poseAvatar.props[x].clone();
1329
+ }
1330
+
1331
+ // Make sure the target has the delta properties, because we need it as a basis
1332
+ if ( this.poseDelta.props.hasOwnProperty(x) && !this.poseTarget.props.hasOwnProperty(x) ) {
1333
+ this.poseTarget.props[x] = this.poseAvatar.props[x].clone();
1334
+ }
1335
+
1336
+ // Take target pose
1337
+ this.poseTarget.props[x].t = this.animClock;
1338
+ this.poseTarget.props[x].d = 2000;
1339
+ });
1340
+
1341
+ // Reset IK bone positions
1342
+ this.ikMesh.traverse( x => {
1343
+ if (x.isBone) {
1344
+ x.position.copy( this.armature.getObjectByName(x.name).position );
1345
+ }
1346
+ });
1347
+
1348
+ if ( this.isAvatarOnly ) {
1349
+ if ( this.scene ) {
1350
+ this.scene.add( this.armature );
1351
+ }
1352
+ } else {
1353
+ // Add avatar to scene
1354
+ this.scene.add(gltf.scene);
1355
+
1356
+ // Add lights
1357
+ this.scene.add( this.lightAmbient );
1358
+ this.scene.add( this.lightDirect );
1359
+ this.scene.add( this.lightSpot );
1360
+ this.lightSpot.target = this.armature.getObjectByName('Head');
1361
+ }
1362
+
1363
+ // Setup Dynamic Bones
1364
+ if ( avatar.hasOwnProperty("modelDynamicBones") ) {
1365
+ try {
1366
+ this.dynamicbones.setup(this.scene, this.armature, avatar.modelDynamicBones );
1367
+ }
1368
+ catch(error) {
1369
+ console.error("Dynamic bones setup failed: " + error);
1370
+ }
1371
+ }
1372
+
1373
+ // Find objects that we need in the animate function
1374
+ this.objectLeftToeBase = this.armature.getObjectByName('LeftToeBase');
1375
+ this.objectRightToeBase = this.armature.getObjectByName('RightToeBase');
1376
+ this.objectLeftEye = this.armature.getObjectByName('LeftEye');
1377
+ this.objectRightEye = this.armature.getObjectByName('RightEye');
1378
+ this.objectLeftArm = this.armature.getObjectByName('LeftArm');
1379
+ this.objectRightArm = this.armature.getObjectByName('RightArm');
1380
+ this.objectHips = this.armature.getObjectByName('Hips');
1381
+ this.objectHead = this.armature.getObjectByName('Head');
1382
+ this.objectNeck = this.armature.getObjectByName('Neck');
1383
+
1384
+ // Estimate avatar height based on eye level
1385
+ const plEye = new THREE.Vector3();
1386
+ this.objectLeftEye.getWorldPosition(plEye);
1387
+ this.avatarHeight = plEye.y + 0.2;
1388
+
1389
+ // Set pose, view and start animation
1390
+ if ( !this.viewName ) this.setView( this.opt.cameraView );
1391
+ this.setMood( this.avatar.avatarMood || this.moodName || this.opt.avatarMood );
1392
+ this.start();
1393
+
1394
+ }
1395
+
1396
+ /**
1397
+ * Get view names.
1398
+ * @return {string[]} Supported view names.
1399
+ */
1400
+ getViewNames() {
1401
+ return ['full', 'mid', 'upper', 'head'];
1402
+ }
1403
+
1404
+ /**
1405
+ * Get current view.
1406
+ * @return {string} View name.
1407
+ */
1408
+ getView() {
1409
+ return this.viewName;
1410
+ }
1411
+
1412
+ /**
1413
+ * Fit 3D object to the view.
1414
+ * @param {string} [view=null] Camera view. If null, reset current view
1415
+ * @param {Object} [opt=null] Options
1416
+ */
1417
+ setView(view, opt = null) {
1418
+ view = view || this.viewName;
1419
+ if ( view !== 'full' && view !== 'upper' && view !== 'head' && view !== 'mid' ) return;
1420
+ if ( !this.armature ) {
1421
+ this.opt.cameraView = view;
1422
+ return;
1423
+ }
1424
+
1425
+ this.viewName = view || this.viewName;
1426
+ opt = opt || {};
1427
+
1428
+ // In avatarOnly mode we do not control the camera
1429
+ if ( this.isAvatarOnly ) return;
1430
+
1431
+ // Camera controls
1432
+ const cameraX = opt.hasOwnProperty("cameraX") ? opt.cameraX : this.opt.cameraX;
1433
+ const cameraY = opt.hasOwnProperty("cameraY") ? opt.cameraY : this.opt.cameraY;
1434
+ const cameraDistance = opt.hasOwnProperty("cameraDistance") ? opt.cameraDistance : this.opt.cameraDistance;
1435
+ const cameraRotateX = opt.hasOwnProperty("cameraRotateX") ? opt.cameraRotateX : this.opt.cameraRotateX;
1436
+ const cameraRotateY = opt.hasOwnProperty("cameraRotateY") ? opt.cameraRotateY : this.opt.cameraRotateY;
1437
+
1438
+ const fov = this.camera.fov * ( Math.PI / 180 );
1439
+ let x = - cameraX * Math.tan( fov / 2 );
1440
+ let y = ( 1 - cameraY) * Math.tan( fov / 2 );
1441
+ let z = cameraDistance;
1442
+
1443
+ switch(this.viewName) {
1444
+ case 'head':
1445
+ z += 2;
1446
+ y = y * z + 4 * this.avatarHeight / 5;
1447
+ break;
1448
+ case 'upper':
1449
+ z += 4.5;
1450
+ y = y * z + 2 * this.avatarHeight / 3;
1451
+ break;
1452
+ case 'mid':
1453
+ z += 8;
1454
+ y = y * z + this.avatarHeight / 3;
1455
+ break;
1456
+ default:
1457
+ z += 12;
1458
+ y = y * z;
1459
+ }
1460
+
1461
+ x = x * z;
1462
+
1463
+ this.controlsEnd = new THREE.Vector3(x, y, 0);
1464
+ this.cameraEnd = new THREE.Vector3(x, y, z).applyEuler( new THREE.Euler( cameraRotateX, cameraRotateY, 0 ) );
1465
+
1466
+ if ( this.cameraClock === null ) {
1467
+ this.controls.target.copy( this.controlsEnd );
1468
+ this.camera.position.copy( this.cameraEnd );
1469
+ }
1470
+ this.controlsStart = this.controls.target.clone();
1471
+ this.cameraStart = this.camera.position.clone();
1472
+ this.cameraClock = 0;
1473
+
1474
+ }
1475
+
1476
+ /**
1477
+ * Change light colors and intensities.
1478
+ * @param {Object} opt Options
1479
+ */
1480
+ setLighting(opt) {
1481
+ if ( this.isAvatarOnly ) return;
1482
+ opt = opt || {};
1483
+
1484
+ // Ambient light
1485
+ if ( opt.hasOwnProperty("lightAmbientColor") ) {
1486
+ this.lightAmbient.color.set( new THREE.Color( opt.lightAmbientColor ) );
1487
+ }
1488
+ if ( opt.hasOwnProperty("lightAmbientIntensity") ) {
1489
+ this.lightAmbient.intensity = opt.lightAmbientIntensity;
1490
+ this.lightAmbient.visible = (opt.lightAmbientIntensity !== 0);
1491
+ }
1492
+
1493
+ // Directional light
1494
+ if ( opt.hasOwnProperty("lightDirectColor") ) {
1495
+ this.lightDirect.color.set( new THREE.Color( opt.lightDirectColor ) );
1496
+ }
1497
+ if ( opt.hasOwnProperty("lightDirectIntensity") ) {
1498
+ this.lightDirect.intensity = opt.lightDirectIntensity;
1499
+ this.lightDirect.visible = (opt.lightDirectIntensity !== 0);
1500
+ }
1501
+ if ( opt.hasOwnProperty("lightDirectPhi") && opt.hasOwnProperty("lightDirectTheta") ) {
1502
+ this.lightDirect.position.setFromSphericalCoords(2, opt.lightDirectPhi, opt.lightDirectTheta);
1503
+ }
1504
+
1505
+ // Spot light
1506
+ if ( opt.hasOwnProperty("lightSpotColor") ) {
1507
+ this.lightSpot.color.set( new THREE.Color( opt.lightSpotColor ) );
1508
+ }
1509
+ if ( opt.hasOwnProperty("lightSpotIntensity") ) {
1510
+ this.lightSpot.intensity = opt.lightSpotIntensity;
1511
+ this.lightSpot.visible = (opt.lightSpotIntensity !== 0);
1512
+ }
1513
+ if ( opt.hasOwnProperty("lightSpotPhi") && opt.hasOwnProperty("lightSpotTheta") ) {
1514
+ this.lightSpot.position.setFromSphericalCoords( 2, opt.lightSpotPhi, opt.lightSpotTheta );
1515
+ this.lightSpot.position.add( new THREE.Vector3(0,1.5,0) );
1516
+ }
1517
+ if ( opt.hasOwnProperty("lightSpotDispersion") ) {
1518
+ this.lightSpot.angle = opt.lightSpotDispersion;
1519
+ }
1520
+ }
1521
+
1522
+ /**
1523
+ * Render scene.
1524
+ */
1525
+ render() {
1526
+ if ( this.isRunning && !this.isAvatarOnly ) {
1527
+ this.renderer.render( this.scene, this.camera );
1528
+ }
1529
+ }
1530
+
1531
+ /**
1532
+ * Resize avatar.
1533
+ */
1534
+ onResize() {
1535
+ if ( !this.isAvatarOnly ) {
1536
+ this.camera.aspect = this.nodeAvatar.clientWidth / this.nodeAvatar.clientHeight;
1537
+ this.camera.updateProjectionMatrix();
1538
+ this.renderer.setSize( this.nodeAvatar.clientWidth, this.nodeAvatar.clientHeight );
1539
+ this.controls.update();
1540
+ this.render();
1541
+ }
1542
+ }
1543
+
1544
+ /**
1545
+ * Update avatar pose.
1546
+ * @param {number} t High precision timestamp in ms.
1547
+ */
1548
+ updatePoseBase(t) {
1549
+ for( const [key,val] of Object.entries(this.poseTarget.props) ) {
1550
+ const o = this.poseAvatar.props[key];
1551
+ if (o) {
1552
+ let alpha = (t - val.t) / val.d;
1553
+ if ( alpha > 1 || !this.poseBase.props.hasOwnProperty(key) ) {
1554
+ o.copy(val);
1555
+ } else {
1556
+ if ( o.isQuaternion ) {
1557
+ o.copy( this.poseBase.props[key].slerp(val, this.easing(alpha) ));
1558
+ } else if ( o.isVector3 ) {
1559
+ o.copy( this.poseBase.props[key].lerp(val, this.easing(alpha) ));
1560
+ }
1561
+ }
1562
+ }
1563
+ }
1564
+ }
1565
+
1566
+ /**
1567
+ * Update avatar pose deltas
1568
+ */
1569
+ updatePoseDelta() {
1570
+ for( const [key,d] of Object.entries(this.poseDelta.props) ) {
1571
+ if ( d.x === 0 && d.y === 0 && d.z === 0 ) continue;
1572
+ e.set(d.x,d.y,d.z);
1573
+ const o = this.poseAvatar.props[key];
1574
+ if ( o.isQuaternion ) {
1575
+ q.setFromEuler(e);
1576
+ o.multiply(q);
1577
+ } else if ( o.isVector3 ) {
1578
+ o.add( e );
1579
+ }
1580
+ }
1581
+ }
1582
+
1583
+ /**
1584
+ * Update morph target values.
1585
+ * @param {number} dt Delta time in ms.
1586
+ */
1587
+ updateMorphTargets(dt) {
1588
+
1589
+ for( let [mt,o] of Object.entries(this.mtAvatar) ) {
1590
+
1591
+ if ( !o.needsUpdate ) continue;
1592
+
1593
+ // Alternative target (priority order):
1594
+ // - fixed: Fixed value, typically user controlled
1595
+ // - realtime: Realtime value, overriding everything except fixed
1596
+ // - system: System value, which overrides animations
1597
+ // - newvalue: Animation value
1598
+ // - baseline: Baseline value when none of the above applies
1599
+ let target = null;
1600
+ let newvalue = null;
1601
+ if ( o.fixed !== null ) {
1602
+ target = o.fixed;
1603
+ o.system = null;
1604
+ o.systemd = null;
1605
+ o.newvalue = null;
1606
+ if ( o.ref && o.ref.hasOwnProperty(mt) ) delete o.ref[mt];
1607
+ o.ref = null;
1608
+ o.base = null;
1609
+ if ( o.value === target ) {
1610
+ o.needsUpdate = false;
1611
+ continue;
1612
+ }
1613
+ } else if ( o.realtime !== null ) {
1614
+ o.ref = null;
1615
+ o.base = null;
1616
+ newvalue = o.realtime;
1617
+ } else if ( o.system !== null ) {
1618
+ target = o.system;
1619
+ o.newvalue = null;
1620
+ if ( o.ref && o.ref.hasOwnProperty(mt) ) delete o.ref[mt];
1621
+ o.ref = null;
1622
+ o.base = null;
1623
+ if ( o.systemd !== null ) {
1624
+ if ( o.systemd === 0 ) {
1625
+ target = null;
1626
+ o.system = null;
1627
+ o.systemd = null;
1628
+ } else {
1629
+ o.systemd -= dt;
1630
+ if ( o.systemd < 0 ) o.systemd= 0;
1631
+ if ( o.value === target ) {
1632
+ target = null;
1633
+ }
1634
+ }
1635
+ } else if ( o.value === target ) {
1636
+ target = null;
1637
+ o.system = null;
1638
+ }
1639
+ } else if ( o.newvalue !== null ) {
1640
+ o.ref = null;
1641
+ o.base = null;
1642
+ newvalue = o.newvalue;
1643
+ o.newvalue = null;
1644
+ } else if ( o.base !== null ) {
1645
+ target = o.base;
1646
+ o.ref = null;
1647
+ if ( o.value === target ) {
1648
+ target = null;
1649
+ o.base = null;
1650
+ o.needsUpdate = false;
1651
+ }
1652
+ } else {
1653
+ o.ref = null;
1654
+ if ( o.baseline !== null && o.value !== o.baseline ) {
1655
+ target = o.baseline;
1656
+ o.base = o.baseline;
1657
+ } else {
1658
+ o.needsUpdate = false;
1659
+ }
1660
+ }
1661
+
1662
+ // Calculate new value using exponential smoothing
1663
+ if ( target !== null ) {
1664
+ let diff = target - o.value;
1665
+ if ( diff >= 0 ) {
1666
+ if ( diff < 0.005 ) {
1667
+ newvalue = target;
1668
+ o.v = 0;
1669
+ } else {
1670
+ if ( o.v < o.maxv ) o.v += o.acc * dt;
1671
+ if ( o.v >= 0 ) {
1672
+ newvalue = o.value + diff * ( 1 - Math.exp(- o.v * dt) );
1673
+ } else {
1674
+ newvalue = o.value + o.v * dt * ( 1 - Math.exp(o.v * dt) );
1675
+ }
1676
+ }
1677
+ } else {
1678
+ if ( diff > -0.005 ) {
1679
+ newvalue = target;
1680
+ o.v = 0;
1681
+ } else {
1682
+ if ( o.v > -o.maxv ) o.v -= o.acc * dt;
1683
+ if ( o.v >= 0 ) {
1684
+ newvalue = o.value + o.v * dt * ( 1 - Math.exp(- o.v * dt) );
1685
+ } else {
1686
+ newvalue = o.value + diff * ( 1 - Math.exp( o.v * dt) );
1687
+ }
1688
+ }
1689
+ }
1690
+ }
1691
+
1692
+ // Check limits and whether we need to actually update the morph target
1693
+ if ( o.limit !== null ) {
1694
+ if ( newvalue !== null && newvalue !== o.value ) {
1695
+ o.value = newvalue;
1696
+ if ( o.onchange !== null ) o.onchange(newvalue);
1697
+ }
1698
+ newvalue = o.limit(o.value);
1699
+ if ( newvalue === o.applied ) continue;
1700
+ } else {
1701
+ if ( newvalue === null || newvalue === o.value ) continue;
1702
+ o.value = newvalue;
1703
+ if ( o.onchange !== null ) o.onchange(newvalue);
1704
+ }
1705
+
1706
+ o.applied = newvalue;
1707
+ if ( o.applied < o.min ) o.applied = o.min;
1708
+ if ( o.applied > o.max ) o.applied = o.max;
1709
+
1710
+ // Apply value
1711
+ switch(mt) {
1712
+
1713
+ case 'headRotateX':
1714
+ this.poseDelta.props['Head.quaternion'].x = o.applied + this.mtAvatar['bodyRotateX'].applied;
1715
+ break;
1716
+
1717
+ case 'headRotateY':
1718
+ this.poseDelta.props['Head.quaternion'].y = o.applied + this.mtAvatar['bodyRotateY'].applied;
1719
+ break;
1720
+
1721
+ case 'headRotateZ':
1722
+ this.poseDelta.props['Head.quaternion'].z = o.applied + this.mtAvatar['bodyRotateZ'].applied;
1723
+ break;
1724
+
1725
+ case 'bodyRotateX':
1726
+ this.poseDelta.props['Head.quaternion'].x = o.applied + this.mtAvatar['headRotateX'].applied;
1727
+ this.poseDelta.props['Spine1.quaternion'].x = o.applied/2;
1728
+ this.poseDelta.props['Spine.quaternion'].x = o.applied/8;
1729
+ this.poseDelta.props['Hips.quaternion'].x = o.applied/24;
1730
+ break;
1731
+
1732
+ case 'bodyRotateY':
1733
+ this.poseDelta.props['Head.quaternion'].y = o.applied + this.mtAvatar['headRotateY'].applied;
1734
+ this.poseDelta.props['Spine1.quaternion'].y = o.applied/2;
1735
+ this.poseDelta.props['Spine.quaternion'].y = o.applied/2;
1736
+ this.poseDelta.props['Hips.quaternion'].y = o.applied/4;
1737
+ this.poseDelta.props['LeftUpLeg.quaternion'].y = o.applied/2;
1738
+ this.poseDelta.props['RightUpLeg.quaternion'].y = o.applied/2;
1739
+ this.poseDelta.props['LeftLeg.quaternion'].y = o.applied/4;
1740
+ this.poseDelta.props['RightLeg.quaternion'].y = o.applied/4;
1741
+ break;
1742
+
1743
+ case 'bodyRotateZ':
1744
+ this.poseDelta.props['Head.quaternion'].z = o.applied + this.mtAvatar['headRotateZ'].applied;
1745
+ this.poseDelta.props['Spine1.quaternion'].z = o.applied/12;
1746
+ this.poseDelta.props['Spine.quaternion'].z = o.applied/12;
1747
+ this.poseDelta.props['Hips.quaternion'].z = o.applied/24;
1748
+ break;
1749
+
1750
+ case 'handFistLeft':
1751
+ case 'handFistRight':
1752
+ const side = mt.substring(8);
1753
+ ['HandThumb', 'HandIndex','HandMiddle',
1754
+ 'HandRing', 'HandPinky'].forEach( (x,i) => {
1755
+ if ( i === 0 ) {
1756
+ this.poseDelta.props[side+x+'1.quaternion'].x = 0;
1757
+ this.poseDelta.props[side+x+'2.quaternion'].z = (side === 'Left' ? -1 : 1) * o.applied;
1758
+ this.poseDelta.props[side+x+'3.quaternion'].z = (side === 'Left' ? -1 : 1) * o.applied;
1759
+ } else {
1760
+ this.poseDelta.props[side+x+'1.quaternion'].x = o.applied;
1761
+ this.poseDelta.props[side+x+'2.quaternion'].x = 1.5 * o.applied;
1762
+ this.poseDelta.props[side+x+'3.quaternion'].x = 1.5 * o.applied;
1763
+ }
1764
+ });
1765
+ break;
1766
+
1767
+ case 'chestInhale':
1768
+ const scale = o.applied/20;
1769
+ const d = { x: scale, y: (scale/2), z: (3 * scale) };
1770
+ const dneg = { x: (1/(1+scale) - 1), y: (1/(1 + scale/2) - 1), z: (1/(1 + 3 * scale) - 1) };
1771
+ this.poseDelta.props['Spine1.scale'] = d;
1772
+ this.poseDelta.props['Neck.scale'] = dneg;
1773
+ this.poseDelta.props['LeftArm.scale'] = dneg;
1774
+ this.poseDelta.props['RightArm.scale'] = dneg;
1775
+ break;
1776
+
1777
+ default:
1778
+ for( let i=0,l=o.ms.length; i<l; i++ ) {
1779
+ o.ms[i][o.is[i]] = o.applied;
1780
+ }
1781
+
1782
+ }
1783
+ }
1784
+ }
1785
+
1786
+ /**
1787
+ * Get given pose as a string.
1788
+ * @param {Object} pose Pose
1789
+ * @param {number} [prec=1000] Precision used in values
1790
+ * @return {string} Pose as a string
1791
+ */
1792
+ getPoseString(pose,prec=1000){
1793
+ let s = '{';
1794
+ Object.entries(pose).forEach( (x,i) => {
1795
+ const ids = x[0].split('.');
1796
+ if ( ids[1] === 'position' || ids[1] === 'rotation' || ids[1] === 'quaternion' ) {
1797
+ const key = (ids[1] === 'quaternion' ? (ids[0]+'.rotation') : x[0]);
1798
+ const val = (x[1].isQuaternion ? new THREE.Euler().setFromQuaternion(x[1]) : x[1]);
1799
+ s += (i?", ":"") + "'" + key + "':{";
1800
+ s += 'x:' + Math.round(val.x * prec) / prec;
1801
+ s += ', y:' + Math.round(val.y * prec) / prec;
1802
+ s += ', z:' + Math.round(val.z * prec) / prec;
1803
+ s += '}';
1804
+ }
1805
+ });
1806
+ s += '}';
1807
+ return s;
1808
+ }
1809
+
1810
+
1811
+ /**
1812
+ * Return pose template property taking into account mirror pose and gesture.
1813
+ * @param {string} key Property key
1814
+ * @return {Quaternion|Vector3} Position or rotation
1815
+ */
1816
+ getPoseTemplateProp(key) {
1817
+
1818
+ const ids = key.split('.');
1819
+ let target = ids[0] + '.' + (ids[1] === 'rotation' ? 'quaternion' : ids[1]);
1820
+
1821
+ if ( this.gesture && this.gesture.hasOwnProperty(target) ) {
1822
+ return this.gesture[target].clone();
1823
+ } else {
1824
+ let source = ids[0] + '.' + (ids[1] === 'quaternion' ? 'rotation' : ids[1]);
1825
+ if ( !this.poseWeightOnLeft ) {
1826
+ if ( source.startsWith('Left') ) {
1827
+ source = 'Right' + source.substring(4);
1828
+ target = 'Right' + target.substring(4);
1829
+ } else if ( source.startsWith('Right') ) {
1830
+ source = 'Left' + source.substring(5);
1831
+ target = 'Left' + target.substring(5);
1832
+ }
1833
+ }
1834
+
1835
+ // Get value
1836
+ let val;
1837
+ if ( this.poseTarget.template.props.hasOwnProperty(target) ) {
1838
+ const o = {};
1839
+ o[target] = this.poseTarget.template.props[target];
1840
+ val = this.propsToThreeObjects( o )[target];
1841
+ } else if ( this.poseTarget.template.props.hasOwnProperty(source) ) {
1842
+ const o = {};
1843
+ o[source] = this.poseTarget.template.props[source];
1844
+ val = this.propsToThreeObjects( o )[target];
1845
+ }
1846
+
1847
+ // Mirror
1848
+ if ( val && !this.poseWeightOnLeft && val.isQuaternion ) {
1849
+ val.x *= -1;
1850
+ val.w *= -1;
1851
+ }
1852
+
1853
+ return val;
1854
+ }
1855
+ }
1856
+
1857
+ /**
1858
+ * Change body weight from current leg to another.
1859
+ * @param {Object} p Pose properties
1860
+ * @return {Object} Mirrored pose.
1861
+ */
1862
+ mirrorPose(p) {
1863
+ const r = {};
1864
+ for( let [key,val] of Object.entries(p) ) {
1865
+
1866
+ // Create a mirror image
1867
+ if ( val.isQuaternion ) {
1868
+ if ( key.startsWith('Left') ) {
1869
+ key = 'Right' + key.substring(4);
1870
+ } else if ( key.startsWith('Right') ) {
1871
+ key = 'Left' + key.substring(5);
1872
+ }
1873
+ val.x *= -1;
1874
+ val.w *= -1;
1875
+ }
1876
+
1877
+ r[key] = val.clone();
1878
+
1879
+ // Custom properties
1880
+ r[key].t = val.t;
1881
+ r[key].d = val.d;
1882
+ }
1883
+ return r;
1884
+ }
1885
+
1886
+ /**
1887
+ * Create a new pose.
1888
+ * @param {Object} template Pose template
1889
+ * @param {numeric} [ms=2000] Transition duration in ms
1890
+ * @return {Object} A new pose object.
1891
+ */
1892
+ poseFactory(template, ms=2000) {
1893
+
1894
+ // Pose object
1895
+ const o = {
1896
+ template: template,
1897
+ props: this.propsToThreeObjects( template.props )
1898
+ };
1899
+
1900
+ for( const [p,val] of Object.entries(o.props) ) {
1901
+
1902
+ // Restrain movement when standing
1903
+ if ( this.opt.modelMovementFactor < 1 && template.standing &&
1904
+ (p === 'Hips.quaternion' || p === 'Spine.quaternion' ||
1905
+ p === 'Spine1.quaternion' || p === 'Spine2.quaternion' ||
1906
+ p === 'Neck.quaternion' || p === 'LeftUpLeg.quaternion' ||
1907
+ p === 'LeftLeg.quaternion' || p === 'RightUpLeg.quaternion' ||
1908
+ p === 'RightLeg.quaternion') ) {
1909
+ const ref = this.poseStraight[p];
1910
+ const angle = val.angleTo( ref );
1911
+ val.rotateTowards( ref, (1 - this.opt.modelMovementFactor) * angle );
1912
+ }
1913
+
1914
+ // Custom properties
1915
+ val.t = this.animClock; // timestamp
1916
+ val.d = ms; // Transition duration
1917
+
1918
+ }
1919
+ return o;
1920
+ }
1921
+
1922
+ /**
1923
+ * Set a new pose and start transition timer.
1924
+ * @param {Object} template Pose template, if null update current pose
1925
+ * @param {number} [ms=2000] Transition time in milliseconds
1926
+ */
1927
+ setPoseFromTemplate(template, ms=2000) {
1928
+
1929
+ // Special cases
1930
+ const isIntermediate = template && this.poseTarget && this.poseTarget.template && ((this.poseTarget.template.standing && template.lying) || (this.poseTarget.template.lying && template.standing));
1931
+ const isSameTemplate = template && (template === this.poseCurrentTemplate);
1932
+ const isWeightOnLeft = this.poseWeightOnLeft;
1933
+ let duration = isIntermediate ? 1000 : ms;
1934
+
1935
+ // New pose template
1936
+ if ( isIntermediate) {
1937
+ this.poseCurrentTemplate = this.poseTemplates['oneknee'];
1938
+ setTimeout( () => {
1939
+ this.setPoseFromTemplate(template,ms);
1940
+ }, duration);
1941
+ } else {
1942
+ this.poseCurrentTemplate = template || this.poseCurrentTemplate;
1943
+ }
1944
+
1945
+ // Set target
1946
+ this.poseTarget = this.poseFactory(this.poseCurrentTemplate, duration);
1947
+ this.poseWeightOnLeft = true;
1948
+
1949
+ // Mirror properties, if necessary
1950
+ if ( (!isSameTemplate && !isWeightOnLeft) || (isSameTemplate && isWeightOnLeft ) ) {
1951
+ this.poseTarget.props = this.mirrorPose(this.poseTarget.props);
1952
+ this.poseWeightOnLeft = !this.poseWeightOnLeft;
1953
+ }
1954
+
1955
+ // Gestures
1956
+ if ( this.gesture ) {
1957
+ for( let [p,val] of Object.entries(this.gesture) ) {
1958
+ if ( this.poseTarget.props.hasOwnProperty(p) ) {
1959
+ this.poseTarget.props[p].copy(val);
1960
+ this.poseTarget.props[p].t = val.t;
1961
+ this.poseTarget.props[p].d = val.d;
1962
+ }
1963
+ }
1964
+ }
1965
+
1966
+ // Make sure deltas are included in the target
1967
+ Object.keys(this.poseDelta.props).forEach( key => {
1968
+ if ( !this.poseTarget.props.hasOwnProperty(key) ) {
1969
+ this.poseTarget.props[key] = this.poseBase.props[key].clone();
1970
+ this.poseTarget.props[key].t = this.animClock;
1971
+ this.poseTarget.props[key].d = duration;
1972
+ }
1973
+ });
1974
+
1975
+ }
1976
+
1977
+ /**
1978
+ * Get morph target value.
1979
+ * @param {string} mt Morph target
1980
+ * @return {number} Value
1981
+ */
1982
+ getValue(mt) {
1983
+ return this.mtAvatar[mt]?.value;
1984
+ }
1985
+
1986
+ /**
1987
+ * Set morph target value.
1988
+ * @param {string} mt Morph target
1989
+ * @param {number} val Value
1990
+ * @param {number} [ms=null] Transition time in milliseconds.
1991
+ */
1992
+ setValue(mt,val,ms=null) {
1993
+ if ( this.mtAvatar.hasOwnProperty(mt) ) {
1994
+ Object.assign(this.mtAvatar[mt],{ system: val, systemd: ms, needsUpdate: true });
1995
+ }
1996
+ }
1997
+
1998
+
1999
+ /**
2000
+ * Get mood names.
2001
+ * @return {string[]} Mood names.
2002
+ */
2003
+ getMoodNames() {
2004
+ return Object.keys(this.animMoods);
2005
+ }
2006
+
2007
+ /**
2008
+ * Get current mood.
2009
+ * @return {string[]} Mood name.
2010
+ */
2011
+ getMood() {
2012
+ return this.opt.avatarMood;
2013
+ }
2014
+
2015
+ /**
2016
+ * Set mood.
2017
+ * @param {string} s Mood name.
2018
+ */
2019
+ setMood(s) {
2020
+ s = (s || '').trim().toLowerCase();
2021
+ if ( !this.animMoods.hasOwnProperty(s) ) throw new Error("Unknown mood.");
2022
+ this.moodName = s;
2023
+ this.mood = this.animMoods[this.moodName];
2024
+
2025
+ // Reset morph target baseline
2026
+ for( let mt of Object.keys(this.mtAvatar) ) {
2027
+ let val = this.mtBaselineExceptions.hasOwnProperty(mt) ? this.mtBaselineExceptions[mt] : this.mtBaselineDefault;
2028
+ if ( this.mood.baseline.hasOwnProperty(mt) ) {
2029
+ val = this.mood.baseline[mt];
2030
+ } else if ( this.avatar.baseline?.hasOwnProperty(mt) ) {
2031
+ val = this.avatar.baseline[mt];
2032
+ }
2033
+ this.setBaselineValue( mt, val );
2034
+ }
2035
+
2036
+ // Set/replace animations
2037
+ this.mood.anims.forEach( x => {
2038
+ let i = this.animQueue.findIndex( y => y.template.name === x.name );
2039
+ if ( i !== -1 ) {
2040
+ this.animQueue.splice(i, 1);
2041
+ }
2042
+ this.animQueue.push( this.animFactory( x, -1 ) );
2043
+ });
2044
+
2045
+ }
2046
+
2047
+
2048
+ /**
2049
+ * Get morph target names.
2050
+ * @return {string[]} Morph target names.
2051
+ */
2052
+ getMorphTargetNames() {
2053
+ return [ 'eyesRotateX', 'eyesRotateY', ...Object.keys(this.mtAvatar)].sort();
2054
+ }
2055
+
2056
+ /**
2057
+ * Get baseline value for the morph target.
2058
+ * @param {string} mt Morph target name
2059
+ * @return {number} Value, null if not in baseline
2060
+ */
2061
+ getBaselineValue( mt ) {
2062
+ if ( mt === 'eyesRotateY' ) {
2063
+ const ll = this.getBaselineValue('eyeLookOutLeft');
2064
+ if ( ll === undefined ) return undefined;
2065
+ const lr = this.getBaselineValue('eyeLookInLeft');
2066
+ if ( lr === undefined ) return undefined;
2067
+ const rl = this.getBaselineValue('eyeLookOutRight');
2068
+ if ( rl === undefined ) return undefined;
2069
+ const rr = this.getBaselineValue('eyeLookInRight');
2070
+ if ( rr === undefined ) return undefined;
2071
+ return ll - lr;
2072
+ } else if ( mt === 'eyesRotateX' ) {
2073
+ const d = this.getBaselineValue('eyesLookDown');
2074
+ if ( d === undefined ) return undefined;
2075
+ const u = this.getBaselineValue('eyesLookUp');
2076
+ if ( u === undefined ) return undefined;
2077
+ return d - u;
2078
+ } else {
2079
+ return this.mtAvatar[mt]?.baseline;
2080
+ }
2081
+ }
2082
+
2083
+ /**
2084
+ * Set baseline for morph target.
2085
+ * @param {string} mt Morph target name
2086
+ * @param {number} val Value, null if to be removed from baseline
2087
+ */
2088
+ setBaselineValue( mt, val ) {
2089
+ if ( mt === 'eyesRotateY' ) {
2090
+ this.setBaselineValue('eyeLookOutLeft', (val === null) ? null : (val>0 ? val : 0) );
2091
+ this.setBaselineValue('eyeLookInLeft', (val === null) ? null : (val>0 ? 0 : -val) );
2092
+ this.setBaselineValue('eyeLookOutRight', (val === null) ? null : (val>0 ? 0 : -val) );
2093
+ this.setBaselineValue('eyeLookInRight', (val === null) ? null : (val>0 ? val : 0) );
2094
+ } else if ( mt === 'eyesRotateX' ) {
2095
+ this.setBaselineValue('eyesLookDown', (val === null) ? null : (val>0 ? val : 0) );
2096
+ this.setBaselineValue('eyesLookUp', (val === null) ? null : (val>0 ? 0 : -val) );
2097
+ } else {
2098
+ if ( this.mtAvatar.hasOwnProperty(mt) ) {
2099
+ Object.assign(this.mtAvatar[mt],{ base: null, baseline: val, needsUpdate: true });
2100
+ }
2101
+ }
2102
+ }
2103
+
2104
+ /**
2105
+ * Get fixed value for the morph target.
2106
+ * @param {string} mt Morph target name
2107
+ * @return {number} Value, null if not fixed
2108
+ */
2109
+ getFixedValue( mt ) {
2110
+ if ( mt === 'eyesRotateY' ) {
2111
+ const ll = this.getFixedValue('eyeLookOutLeft');
2112
+ if ( ll === null ) return null;
2113
+ const lr = this.getFixedValue('eyeLookInLeft');
2114
+ if ( lr === null ) return null;
2115
+ const rl = this.getFixedValue('eyeLookOutRight');
2116
+ if ( rl === null ) return null;
2117
+ const rr = this.getFixedValue('eyeLookInRight');
2118
+ if ( rr === null ) return null;
2119
+ return ll - lr;
2120
+ } else if ( mt === 'eyesRotateX' ) {
2121
+ const d = this.getFixedValue('eyesLookDown');
2122
+ if ( d === null ) return null;
2123
+ const u = this.getFixedValue('eyesLookUp');
2124
+ if ( u === null ) return null;
2125
+ return d - u;
2126
+ } else {
2127
+ return this.mtAvatar[mt]?.fixed;
2128
+ }
2129
+ }
2130
+
2131
+ /**
2132
+ * Fix morph target.
2133
+ * @param {string} mt Morph target name
2134
+ * @param {number} val Value, null if to be removed
2135
+ */
2136
+ setFixedValue( mt, val, ms=null ) {
2137
+ if ( mt === 'eyesRotateY' ) {
2138
+ this.setFixedValue('eyeLookOutLeft', (val === null) ? null : (val>0 ? val : 0 ), ms );
2139
+ this.setFixedValue('eyeLookInLeft', (val === null) ? null : (val>0 ? 0 : -val ), ms );
2140
+ this.setFixedValue('eyeLookOutRight', (val === null) ? null : (val>0 ? 0 : -val ), ms );
2141
+ this.setFixedValue('eyeLookInRight', (val === null) ? null : (val>0 ? val : 0 ), ms );
2142
+ } else if ( mt === 'eyesRotateX' ) {
2143
+ this.setFixedValue('eyesLookDown', (val === null) ? null : (val>0 ? val : 0 ), ms );
2144
+ this.setFixedValue('eyesLookUp', (val === null) ? null : (val>0 ? 0 : -val ), ms );
2145
+ } else {
2146
+ if ( this.mtAvatar.hasOwnProperty(mt) ) {
2147
+ Object.assign(this.mtAvatar[mt],{ fixed: val, needsUpdate: true });
2148
+ }
2149
+ }
2150
+ }
2151
+
2152
+
2153
+ /**
2154
+ * Create a new animation based on an animation template.
2155
+ * @param {Object} t Animation template
2156
+ * @param {number} [loop=false] Number of loops, false if not looped
2157
+ * @param {number} [scaleTime=1] Scale template times
2158
+ * @param {number} [scaleValue=1] Scale template values
2159
+ * @param {boolean} [noClockOffset=false] Do not apply clock offset
2160
+ * @return {Object} New animation object.
2161
+ */
2162
+ animFactory( t, loop = false, scaleTime = 1, scaleValue = 1, noClockOffset = false ) {
2163
+ const o = { template: t, ts: [0], vs: {} };
2164
+
2165
+ // Follow the hierarchy of objects
2166
+ let a = t;
2167
+ while(1) {
2168
+ if ( a.hasOwnProperty(this.stateName) ) {
2169
+ a = a[this.stateName];
2170
+ } else if ( a.hasOwnProperty(this.moodName) ) {
2171
+ a = a[this.moodName];
2172
+ } else if ( a.hasOwnProperty(this.poseName) ) {
2173
+ a = a[this.poseName];
2174
+ } else if ( a.hasOwnProperty(this.viewName) ) {
2175
+ a = a[this.viewName];
2176
+ } else if ( this.avatar.body && a.hasOwnProperty(this.avatar.body) ) {
2177
+ a = a[this.avatar.body];
2178
+ } else if ( a.hasOwnProperty('alt') ) {
2179
+
2180
+ // Go through alternatives with probabilities
2181
+ let b = a.alt[0];
2182
+ if ( a.alt.length > 1 ) {
2183
+ // Flip a coin
2184
+ const coin = Math.random();
2185
+ let p = 0;
2186
+ for( let i=0; i<a.alt.length; i++ ) {
2187
+ let val = this.valueFn(a.alt[i].p);
2188
+ p += (val === undefined ? (1-p)/(a.alt.length-1-i) : val);
2189
+ if (coin<p) {
2190
+ b = a.alt[i];
2191
+ break;
2192
+ }
2193
+ }
2194
+ }
2195
+ a = b;
2196
+
2197
+ } else {
2198
+ break;
2199
+ }
2200
+ }
2201
+
2202
+ // Time series
2203
+ let delay = this.valueFn(a.delay) || 0;
2204
+ if ( Array.isArray(delay) ) {
2205
+ delay = this.gaussianRandom(...delay);
2206
+ }
2207
+ if ( a.hasOwnProperty('dt') ) {
2208
+ a.dt.forEach( (x,i) => {
2209
+ let val = this.valueFn(x);
2210
+ if ( Array.isArray(val) ) {
2211
+ val = this.gaussianRandom(...val);
2212
+ }
2213
+ o.ts[i+1] = o.ts[i] + val;
2214
+ });
2215
+ } else {
2216
+ let l = Object.values(a.vs).reduce( (acc,val) => (val.length > acc) ? val.length : acc, 0);
2217
+ o.ts = Array(l+1).fill(0);
2218
+ }
2219
+ if ( noClockOffset ) {
2220
+ o.ts = o.ts.map( x => delay + x * scaleTime );
2221
+ } else {
2222
+ o.ts = o.ts.map( x => this.animClock + delay + x * scaleTime );
2223
+ }
2224
+
2225
+ // Values
2226
+ for( let [mt,vs] of Object.entries(a.vs) ) {
2227
+ const base = this.getBaselineValue(mt);
2228
+ const vals = vs.map( x => {
2229
+ x = this.valueFn(x);
2230
+ if ( x === null ) {
2231
+ return null;
2232
+ } else if ( typeof x === 'function' ) {
2233
+ return x;
2234
+ } else if ( typeof x === 'string' || x instanceof String ) {
2235
+ return x.slice();
2236
+ } else if ( Array.isArray(x) ) {
2237
+ if ( mt === 'gesture' ) {
2238
+ return x.slice();
2239
+ } else {
2240
+ return (base === undefined ? 0 : base) + scaleValue * this.gaussianRandom(...x);
2241
+ }
2242
+ } else if (typeof x == "boolean") {
2243
+ return x;
2244
+ } else if ( x instanceof Object && x.constructor === Object ) {
2245
+ return Object.assign( {}, x );
2246
+ } else {
2247
+ return (base === undefined ? 0 : base) + scaleValue * x;
2248
+ }
2249
+ });
2250
+
2251
+ if ( mt === 'eyesRotateY' ) {
2252
+ o.vs['eyeLookOutLeft'] = [null, ...vals.map( x => (x>0) ? x : 0 ) ];
2253
+ o.vs['eyeLookInLeft'] = [null, ...vals.map( x => (x>0) ? 0 : -x ) ];
2254
+ o.vs['eyeLookOutRight'] = [null, ...vals.map( x => (x>0) ? 0 : -x ) ];
2255
+ o.vs['eyeLookInRight'] = [null, ...vals.map( x => (x>0) ? x : 0 ) ];
2256
+ } else if ( mt === 'eyesRotateX' ) {
2257
+ o.vs['eyesLookDown'] = [null, ...vals.map( x => (x>0) ? x : 0 ) ];
2258
+ o.vs['eyesLookUp'] = [null, ...vals.map( x => (x>0) ? 0 : -x ) ];
2259
+ } else {
2260
+ o.vs[mt] = [null, ...vals];
2261
+ }
2262
+ }
2263
+ for( let mt of Object.keys(o.vs) ) {
2264
+ while( o.vs[mt].length <= o.ts.length ) o.vs[mt].push( o.vs[mt][ o.vs[mt].length - 1 ]);
2265
+ }
2266
+
2267
+ // Mood
2268
+ if ( t.hasOwnProperty("mood") ) o.mood = this.valueFn(t.mood).slice();
2269
+
2270
+ // Loop
2271
+ if ( loop ) o.loop = loop;
2272
+
2273
+ return o;
2274
+ }
2275
+
2276
+ /**
2277
+ * Calculate the correct value based on a given time using the given function.
2278
+ * @param {number[]} vstart Start value
2279
+ * @param {number[]} vend End value
2280
+ * @param {number[]} tstart Start time
2281
+ * @param {number[]} tend End time
2282
+ * @param {number[]} t Current time
2283
+ * @param {function} [fun=null] Ease in/out function, null = linear
2284
+ * @return {number} Value based on the given time.
2285
+ */
2286
+ valueAnimationSeq(vstart,vend,tstart,tend,t,fun=null) {
2287
+ vstart = this.valueFn(vstart);
2288
+ vend = this.valueFn(vend);
2289
+ if ( t < tstart ) t = tstart;
2290
+ if ( t > tend ) t = tend;
2291
+ let k = (vend - vstart) / (tend - tstart);
2292
+ if ( fun ) {
2293
+ k *= fun( ( t - tstart ) / (tend - tstart) );
2294
+ }
2295
+ return k * t + (vstart - k * tstart);
2296
+ }
2297
+
2298
+ /**
2299
+ * Return gaussian distributed random value between start and end with skew.
2300
+ * @param {number} start Start value
2301
+ * @param {number} end End value
2302
+ * @param {number} [skew=1] Skew
2303
+ * @param {number} [samples=5] Number of samples, 1 = uniform distribution.
2304
+ * @return {number} Gaussian random value.
2305
+ */
2306
+ gaussianRandom(start,end,skew=1,samples=5) {
2307
+ let r = 0;
2308
+ for( let i=0; i<samples; i++) r += Math.random();
2309
+ return start + Math.pow(r/samples,skew) * (end - start);
2310
+ }
2311
+
2312
+ /**
2313
+ * Create a sigmoid function.
2314
+ * @param {number} k Sharpness of ease.
2315
+ * @return {function} Sigmoid function.
2316
+ */
2317
+ sigmoidFactory(k) {
2318
+ function base(t) { return (1 / (1 + Math.exp(-k * t))) - 0.5; }
2319
+ var corr = 0.5 / base(1);
2320
+ return function (t) { return corr * base(2 * Math.max(Math.min(t, 1), 0) - 1) + 0.5; };
2321
+ }
2322
+
2323
+ /**
2324
+ * Convert value from one range to another.
2325
+ * @param {number} value Value
2326
+ * @param {number[]} r1 Source range
2327
+ * @param {number[]} r2 Target range
2328
+ * @return {number} Scaled value
2329
+ */
2330
+ convertRange( value, r1, r2 ) {
2331
+ return (value-r1[0]) * (r2[1]-r2[0]) / (r1[1]-r1[0]) + r2[0];
2332
+ }
2333
+
2334
+ /**
2335
+ * Animate the avatar.
2336
+ * @param {number} t High precision timestamp in ms. In avatarOnly mode delta.
2337
+ */
2338
+ animate(t) {
2339
+
2340
+ // Are we running?
2341
+ if ( !this.isRunning ) return;
2342
+
2343
+ let dt;
2344
+ if ( this.isAvatarOnly ) {
2345
+ dt = t;
2346
+ } else {
2347
+ this._raf = requestAnimationFrame( this.animate );
2348
+ dt = t - this.animTimeLast;
2349
+ if ( dt < this.animFrameDur ) return;
2350
+ this.animTimeLast = t;
2351
+ }
2352
+ dt = dt / this.animSlowdownRate;
2353
+ this.animClock += dt;
2354
+
2355
+ let i,j,l,k,vol=0;
2356
+
2357
+ // Statistics start
2358
+ if ( this.stats ) {
2359
+ this.stats.begin();
2360
+ }
2361
+
2362
+ // Listening
2363
+ if ( this.isListening ) {
2364
+
2365
+ // Get input max volume
2366
+ this.listeningAnalyzer.getByteFrequencyData(this.volumeFrequencyData);
2367
+ for (i=2, l=10; i<l; i++) {
2368
+ if (this.volumeFrequencyData[i] > vol) {
2369
+ vol = this.volumeFrequencyData[i];
2370
+ }
2371
+ }
2372
+
2373
+ this.listeningVolume = (this.listeningVolume + vol) / 2;
2374
+ if ( this.listeningActive ) {
2375
+ this.listeningTimerTotal += dt;
2376
+ if ( this.listeningVolume < this.listeningSilenceThresholdLevel ) {
2377
+ this.listeningTimer += dt;
2378
+ if ( this.listeningTimer > this.listeningSilenceThresholdMs ) {
2379
+ if ( this.listeningOnchange ) this.listeningOnchange('stop',this.listeningTimer);
2380
+ this.listeningActive = false;
2381
+ this.listeningTimer = 0;
2382
+ this.listeningTimerTotal = 0;
2383
+ }
2384
+ } else {
2385
+ this.listeningTimer *= 0.5;
2386
+ }
2387
+ if ( this.listeningTimerTotal > this.listeningActiveDurationMax ) {
2388
+ if ( this.listeningOnchange ) this.listeningOnchange('maxactive');
2389
+ this.listeningTimerTotal = 0;
2390
+ }
2391
+ } else {
2392
+ this.listeningTimerTotal += dt;
2393
+ if ( this.listeningVolume > this.listeningActiveThresholdLevel ) {
2394
+ this.listeningTimer += dt;
2395
+ if ( this.listeningTimer > this.listeningActiveThresholdMs ) {
2396
+ if ( this.listeningOnchange ) this.listeningOnchange('start');
2397
+ this.listeningActive = true;
2398
+ this.listeningTimer = 0;
2399
+ this.listeningTimerTotal = 0;
2400
+ }
2401
+ } else {
2402
+ this.listeningTimer *= 0.5;
2403
+ }
2404
+ if ( this.listeningTimerTotal > this.listeningSilenceDurationMax ) {
2405
+ if ( this.listeningOnchange ) this.listeningOnchange('maxsilence');
2406
+ this.listeningTimerTotal = 0;
2407
+ }
2408
+ }
2409
+ }
2410
+
2411
+ // Speaking
2412
+ if ( this.isSpeaking ) {
2413
+ vol = 0;
2414
+ this.audioAnalyzerNode.getByteFrequencyData(this.volumeFrequencyData);
2415
+ for (i=2, l=10; i<l; i++) {
2416
+ if (this.volumeFrequencyData[i] > vol) {
2417
+ vol = this.volumeFrequencyData[i];
2418
+ }
2419
+ }
2420
+ }
2421
+
2422
+ // Animation loop
2423
+ let isEyeContact = null;
2424
+ let isHeadMove = null;
2425
+ const tasks = [];
2426
+ for( i=0, l=this.animQueue.length; i<l; i++ ) {
2427
+ const x = this.animQueue[i];
2428
+ if ( this.animClock < x.ts[0] ) continue;
2429
+
2430
+ for( j = x.ndx || 0, k = x.ts.length; j<k; j++ ) {
2431
+ if ( this.animClock < x.ts[j] ) break;
2432
+
2433
+ for( let [mt,vs] of Object.entries(x.vs) ) {
2434
+
2435
+ if ( this.mtAvatar.hasOwnProperty(mt) ) {
2436
+ if ( vs[j+1] === null ) continue; // Last or unknown target, skip
2437
+
2438
+ // Start value and target
2439
+ const m = this.mtAvatar[mt];
2440
+ if ( vs[j] === null ) vs[j] = m.value; // Fill-in start value
2441
+ if ( j === k - 1 ) {
2442
+ m.newvalue = vs[j];
2443
+ } else {
2444
+ m.newvalue = vs[j+1];
2445
+ const tdiff = x.ts[j+1] - x.ts[j];
2446
+ let alpha = 1;
2447
+ if ( tdiff > 0.0001 ) alpha = (this.animClock - x.ts[j]) / tdiff;
2448
+ if ( alpha < 1 ) {
2449
+ if ( m.easing ) alpha = m.easing(alpha);
2450
+ m.newvalue = ( 1 - alpha ) * vs[j] + alpha * m.newvalue;
2451
+ }
2452
+ if ( m.ref && m.ref !== x.vs && m.ref.hasOwnProperty(mt) ) delete m.ref[mt];
2453
+ m.ref = x.vs;
2454
+ }
2455
+
2456
+ // Volume effect
2457
+ if ( vol ) {
2458
+ switch(mt){
2459
+ case 'viseme_aa':
2460
+ case 'viseme_E':
2461
+ case 'viseme_I':
2462
+ case 'viseme_O':
2463
+ case 'viseme_U':
2464
+ m.newvalue *= 1 + vol / 255 - 0.5;
2465
+ }
2466
+ }
2467
+
2468
+ // Update
2469
+ m.needsUpdate = true;
2470
+
2471
+ } else if ( mt === 'eyeContact' && vs[j] !== null && isEyeContact !== false ) {
2472
+ isEyeContact = Boolean(vs[j]);
2473
+ } else if ( mt === 'headMove' && vs[j] !== null && isHeadMove !== false ) {
2474
+ if ( vs[j] === 0 ) {
2475
+ isHeadMove = false;
2476
+ } else {
2477
+ if ( Math.random() < vs[j] ) isHeadMove = true;
2478
+ vs[j] = null;
2479
+ }
2480
+ } else if ( vs[j] !== null ) {
2481
+ tasks.push({ mt: mt, val: vs[j] });
2482
+ vs[j] = null;
2483
+ }
2484
+
2485
+ }
2486
+
2487
+ }
2488
+
2489
+ // If end timeslot, loop or remove the animation, otherwise keep at it
2490
+ if ( j === k ) {
2491
+ if ( x.hasOwnProperty('mood') ) this.setMood(x.mood);
2492
+ if ( x.loop ) {
2493
+ k = ( this.isSpeaking && (x.template.name === 'head' || x.template.name === 'eyes') ) ? 4 : 1; // Restrain
2494
+ this.animQueue[i] = this.animFactory( x.template, (x.loop > 0 ? x.loop - 1 : x.loop), 1, 1/k );
2495
+ } else {
2496
+ this.animQueue.splice(i--, 1);
2497
+ l--;
2498
+ }
2499
+ } else {
2500
+ x.ndx = j - 1;
2501
+ }
2502
+
2503
+ }
2504
+
2505
+ // Tasks
2506
+ for( let i=0, l=tasks.length; i<l; i++ ) {
2507
+ j = tasks[i].val;
2508
+
2509
+ switch(tasks[i].mt) {
2510
+
2511
+ case 'speak':
2512
+ this.speakText(j);
2513
+ break;
2514
+
2515
+ case 'subtitles':
2516
+ if ( this.onSubtitles && typeof this.onSubtitles === "function" ) {
2517
+ this.onSubtitles(j);
2518
+ }
2519
+ break;
2520
+
2521
+ case 'pose':
2522
+ this.poseName = j;
2523
+ this.setPoseFromTemplate( this.poseTemplates[ this.poseName ] );
2524
+ break;
2525
+
2526
+ case 'gesture':
2527
+ this.playGesture( ...j );
2528
+ break;
2529
+
2530
+ case 'function':
2531
+ if ( j && typeof j === "function" ) {
2532
+ j();
2533
+ }
2534
+ break;
2535
+
2536
+ case 'moveto':
2537
+ Object.entries(j.props).forEach( y => {
2538
+ if ( y[1] ) {
2539
+ this.poseTarget.props[y[0]].copy( y[1] );
2540
+ } else {
2541
+ this.poseTarget.props[y[0]].copy( this.getPoseTemplateProp(y[0]) );
2542
+ }
2543
+ this.poseTarget.props[y[0]].t = this.animClock;
2544
+ this.poseTarget.props[y[0]].d = (y[1] && y[1].d) ? y[1].d : (y.duration || 2000);
2545
+ });
2546
+ break;
2547
+
2548
+ case 'handLeft':
2549
+ this.ikSolve( {
2550
+ iterations: 20, root: "LeftShoulder", effector: "LeftHandMiddle1",
2551
+ links: [
2552
+ { link: "LeftHand", minx: -0.5, maxx: 0.5, miny: -1, maxy: 1, minz: -0.5, maxz: 0.5 },
2553
+ { link: "LeftForeArm", minx: -0.5, maxx: 1.5, miny: -1.5, maxy: 1.5, minz: -0.5, maxz: 3 },
2554
+ { link: "LeftArm", minx: -1.5, maxx: 1.5, miny: 0, maxy: 0, minz: -1, maxz: 3 }
2555
+ ]
2556
+ }, j.x ? new THREE.Vector3(j.x,j.y,j.z) : null, true, j.d );
2557
+ break;
2558
+
2559
+
2560
+ case 'handRight':
2561
+ this.ikSolve( {
2562
+ iterations: 20, root: "RightShoulder", effector: "RightHandMiddle1",
2563
+ links: [
2564
+ { link: "RightHand", minx: -0.5, maxx: 0.5, miny: -1, maxy: 1, minz: -0.5, maxz: 0.5, maxAngle: 0.1 },
2565
+ { link: "RightForeArm", minx: -0.5, maxx: 1.5, miny: -1.5, maxy: 1.5, minz: -3, maxz: 0.5, maxAngle: 0.2 },
2566
+ { link: "RightArm", minx: -1.5, maxx: 1.5, miny: 0, maxy: 0, minz: -1, maxz: 3 }
2567
+ ]
2568
+ }, j.x ? new THREE.Vector3(j.x,j.y,j.z) : null, true, j.d );
2569
+ break;
2570
+ }
2571
+ }
2572
+
2573
+ // Eye contact
2574
+ if ( isEyeContact || isHeadMove ) {
2575
+
2576
+ // Get head position
2577
+ e.setFromQuaternion( this.poseAvatar.props['Head.quaternion'] );
2578
+ e.x = Math.max(-0.9,Math.min(0.9, 2 * e.x - 0.5 ));
2579
+ e.y = Math.max(-0.9,Math.min(0.9, -2.5 * e.y));
2580
+
2581
+ if ( isEyeContact ) {
2582
+ Object.assign( this.mtAvatar['eyesLookDown'], { system: e.x < 0 ? -e.x : 0, needsUpdate: true });
2583
+ Object.assign( this.mtAvatar['eyesLookUp'], { system: e.x < 0 ? 0 : e.x, needsUpdate: true });
2584
+ Object.assign( this.mtAvatar['eyeLookInLeft'], { system: e.y < 0 ? -e.y : 0, needsUpdate: true });
2585
+ Object.assign( this.mtAvatar['eyeLookOutLeft'], { system: e.y < 0 ? 0 : e.y, needsUpdate: true });
2586
+ Object.assign( this.mtAvatar['eyeLookInRight'], { system: e.y < 0 ? 0 : e.y, needsUpdate: true });
2587
+ Object.assign( this.mtAvatar['eyeLookOutRight'], { system: e.y < 0 ? -e.y : 0, needsUpdate: true });
2588
+
2589
+ // Head move
2590
+ if ( isHeadMove ) {
2591
+ i = - this.mtAvatar['bodyRotateY'].value;
2592
+ j = this.gaussianRandom(-0.2,0.2);
2593
+ this.animQueue.push( this.animFactory({ name: "headmove",
2594
+ dt: [[1000,2000],[1000,2000,1,2],[1000,2000],[1000,2000,1,2]], vs: {
2595
+ headRotateY: [i,i,0], headRotateX: [j,j,0], headRotateZ: [-i/4,-i/4,0]
2596
+ }
2597
+ }));
2598
+ }
2599
+
2600
+ } else {
2601
+ i = this.mtAvatar['eyeLookInLeft'].value - this.mtAvatar['eyeLookOutLeft'].value;
2602
+ j = this.gaussianRandom(-0.2,0.2);
2603
+ this.animQueue.push( this.animFactory({ name: "headmove",
2604
+ dt: [[1000,2000],[1000,2000,1,2],[1000,2000],[1000,2000,1,2]], vs: {
2605
+ headRotateY: [null,i,i,0], headRotateX: [null,j,j,0], headRotateZ: [null,-i/4,-i/4,0],
2606
+ eyeLookInLeft: [null,0], eyeLookOutLeft: [null,0], eyeLookInRight: [null,0], eyeLookOutRight: [null,0],
2607
+ eyeContact: [0]
2608
+ }
2609
+ }));
2610
+ }
2611
+
2612
+ }
2613
+
2614
+ // Make sure we do not overshoot
2615
+ if ( dt > 2 * this.animFrameDur ) dt = 2 * this.animFrameDur;
2616
+
2617
+ // Randomize facial expression by changing baseline
2618
+ if ( this.viewName !== 'full' || this.isAvatarOnly) {
2619
+ i = this.mtRandomized[ Math.floor( Math.random() * this.mtRandomized.length ) ];
2620
+ j = this.mtAvatar[i];
2621
+ if ( !j.needsUpdate ) {
2622
+ Object.assign(j,{ base: (this.mood.baseline[i] || 0) + ( 1 + vol/255 ) * Math.random() / 5, needsUpdate: true });
2623
+ }
2624
+ }
2625
+
2626
+ // Animate
2627
+ this.updatePoseBase(this.animClock);
2628
+ if ( this.mixer ) {
2629
+ this.mixer.update(dt / 1000 * this.mixer.timeScale);
2630
+ }
2631
+ this.updatePoseDelta();
2632
+
2633
+
2634
+ // Volume based head movement, set targets
2635
+ if ( (this.isSpeaking || this.isListening) && isEyeContact ) {
2636
+ if ( vol > this.volumeMax ) {
2637
+ this.volumeHeadBase = 0.05;
2638
+ if ( Math.random() > 0.6 ) {
2639
+ this.volumeHeadTarget = - 0.05 - Math.random() / 15;
2640
+ }
2641
+ this.volumeMax = vol;
2642
+ } else {
2643
+ this.volumeMax *= 0.92;
2644
+ this.volumeHeadTarget = this.volumeHeadBase - 0.9 * (this.volumeHeadBase - this.volumeHeadTarget);
2645
+ }
2646
+ } else {
2647
+ this.volumeHeadTarget = 0;
2648
+ this.volumeMax = 0;
2649
+ }
2650
+ i = this.volumeHeadTarget - this.volumeHeadCurrent;
2651
+ j = Math.abs(i);
2652
+ if ( j > 0.0001 ) {
2653
+ k = j * (this.volumeHeadEasing( Math.min(1, this.volumeHeadVelocity * dt / 1000 / j ) / 2 + 0.5 ) - 0.5 );
2654
+ this.volumeHeadCurrent += Math.sign(i) * Math.min(j,k);
2655
+ }
2656
+ if ( Math.abs(this.volumeHeadCurrent) > 0.0001 ) {
2657
+ q.setFromAxisAngle(axisx, this.volumeHeadCurrent );
2658
+ this.objectNeck.quaternion.multiply(q);
2659
+ }
2660
+
2661
+ // Hip-feet balance
2662
+ box.setFromObject( this.armature );
2663
+ this.objectLeftToeBase.getWorldPosition(v);
2664
+ v.sub(this.armature.position);
2665
+ this.objectRightToeBase.getWorldPosition(w);
2666
+ w.sub(this.armature.position);
2667
+ this.objectHips.position.y -= box.min.y / 2;
2668
+ this.objectHips.position.x -= (v.x+w.x)/4;
2669
+ this.objectHips.position.z -= (v.z+w.z)/2;
2670
+
2671
+ // Update Dynamic Bones
2672
+ this.dynamicbones.update(dt);
2673
+
2674
+ // Custom update
2675
+ if ( this.opt.update ) {
2676
+ this.opt.update(dt);
2677
+ }
2678
+
2679
+ // Update morph targets
2680
+ this.updateMorphTargets(dt);
2681
+
2682
+ // Finalize
2683
+ if ( this.isAvatarOnly ) {
2684
+
2685
+ // Statistics end
2686
+ if ( this.stats ) {
2687
+ this.stats.end();
2688
+ }
2689
+
2690
+ } else {
2691
+
2692
+ // Camera
2693
+ if ( this.cameraClock !== null && this.cameraClock < 1000 ) {
2694
+ this.cameraClock += dt;
2695
+ if ( this.cameraClock > 1000 ) this.cameraClock = 1000;
2696
+ let s = new THREE.Spherical().setFromVector3(this.cameraStart);
2697
+ let sEnd = new THREE.Spherical().setFromVector3(this.cameraEnd);
2698
+ s.phi += this.easing(this.cameraClock / 1000) * (sEnd.phi - s.phi);
2699
+ s.theta += this.easing(this.cameraClock / 1000) * (sEnd.theta - s.theta);
2700
+ s.radius += this.easing(this.cameraClock / 1000) * (sEnd.radius - s.radius);
2701
+ s.makeSafe();
2702
+ this.camera.position.setFromSpherical( s );
2703
+ if ( this.controlsStart.x !== this.controlsEnd.x ) {
2704
+ this.controls.target.copy( this.controlsStart.lerp( this.controlsEnd, this.easing(this.cameraClock / 1000) ) );
2705
+ } else {
2706
+ s.setFromVector3(this.controlsStart);
2707
+ sEnd.setFromVector3(this.controlsEnd);
2708
+ s.phi += this.easing(this.cameraClock / 1000) * (sEnd.phi - s.phi);
2709
+ s.theta += this.easing(this.cameraClock / 1000) * (sEnd.theta - s.theta);
2710
+ s.radius += this.easing(this.cameraClock / 1000) * (sEnd.radius - s.radius);
2711
+ s.makeSafe();
2712
+ this.controls.target.setFromSpherical( s );
2713
+ }
2714
+ this.controls.update();
2715
+ }
2716
+
2717
+ // Autorotate
2718
+ if ( this.controls.autoRotate ) this.controls.update();
2719
+
2720
+ // Statistics end
2721
+ if ( this.stats ) {
2722
+ this.stats.end();
2723
+ }
2724
+
2725
+ this.render();
2726
+ }
2727
+
2728
+ }
2729
+
2730
+ /**
2731
+ * Reset all the visemes
2732
+ */
2733
+ resetLips() {
2734
+ this.visemeNames.forEach( x => {
2735
+ this.morphs.forEach( y => {
2736
+ const ndx = y.morphTargetDictionary['viseme_'+x];
2737
+ if ( ndx !== undefined ) {
2738
+ y.morphTargetInfluences[ndx] = 0;
2739
+ }
2740
+ });
2741
+ });
2742
+ }
2743
+
2744
+ /**
2745
+ * Get lip-sync processor based on language. Import module dynamically.
2746
+ * @param {string} lang Language
2747
+ * @param {string} [path="./"] Module path
2748
+ */
2749
+ lipsyncGetProcessor(lang, path="./") {
2750
+ if ( !this.lipsync.hasOwnProperty(lang) ) {
2751
+ const moduleName = path + 'lipsync-' + lang.toLowerCase() + '.mjs';
2752
+ const className = 'Lipsync' + lang.charAt(0).toUpperCase() + lang.slice(1);
2753
+ import(moduleName).then( module => {
2754
+ this.lipsync[lang] = new module[className];
2755
+ });
2756
+ }
2757
+ }
2758
+
2759
+ /**
2760
+ * Preprocess text for tts/lipsync, including:
2761
+ * - convert symbols/numbers to words
2762
+ * - filter out characters that should be left unspoken
2763
+ * @param {string} s Text
2764
+ * @param {string} lang Language
2765
+ * @return {string} Pre-processsed text.
2766
+ */
2767
+ lipsyncPreProcessText(s,lang) {
2768
+ const o = this.lipsync[lang] || Object.values(this.lipsync)[0];
2769
+ return o.preProcessText(s);
2770
+ }
2771
+
2772
+ /**
2773
+ * Convert words to Oculus LipSync Visemes.
2774
+ * @param {string} word Word
2775
+ * @param {string} lang Language
2776
+ * @return {Lipsync} Lipsync object.
2777
+ */
2778
+ lipsyncWordsToVisemes(word,lang) {
2779
+ const o = this.lipsync[lang] || Object.values(this.lipsync)[0];
2780
+ return o.wordsToVisemes(word);
2781
+ }
2782
+
2783
+
2784
+ /**
2785
+ * Add text to the speech queue.
2786
+ * @param {string} s Text.
2787
+ * @param {Options} [opt=null] Text-specific options for lipsync/TTS language, voice, rate and pitch, mood and mute
2788
+ * @param {subtitlesfn} [onsubtitles=null] Callback when a subtitle is written
2789
+ * @param {number[][]} [excludes=null] Array of [start, end] index arrays to not speak
2790
+ */
2791
+ speakText(s, opt = null, onsubtitles = null, excludes = null ) {
2792
+ opt = opt || {};
2793
+
2794
+ // Classifiers
2795
+ const dividersSentence = /[!\.\?\n\p{Extended_Pictographic}]/ug;
2796
+ const dividersWord = /[ ]/ug;
2797
+ const speakables = /[\p{L}\p{N},\.\p{Quotation_Mark}!€\$\+\p{Dash_Punctuation}%&\?]/ug;
2798
+ const emojis = /[\p{Extended_Pictographic}]/ug;
2799
+ const lipsyncLang = opt.lipsyncLang || this.avatar.lipsyncLang || this.opt.lipsyncLang;
2800
+
2801
+ let markdownWord = ''; // markdown word
2802
+ let textWord = ''; // text-to-speech word
2803
+ let markId = 0; // SSML mark id
2804
+ let ttsSentence = []; // Text-to-speech sentence
2805
+ let lipsyncAnim = []; // Lip-sync animation sequence
2806
+ const letters = Array.from(this.segmenter.segment(s), x => x.segment);
2807
+ for( let i=0; i<letters.length; i++ ) {
2808
+ const isLast = i === (letters.length-1);
2809
+ const isSpeakable = letters[i].match(speakables);
2810
+ let isEndOfSentence = letters[i].match(dividersSentence);
2811
+ const isEmoji = letters[i].match(emojis);
2812
+ const isEndOfWord = letters[i].match(dividersWord);
2813
+
2814
+ // Exception for end-of-sentence is repeated dividers
2815
+ if ( isEndOfSentence && !isLast && !isEmoji && letters[i+1].match(dividersSentence) ) {
2816
+ isEndOfSentence = false;
2817
+ }
2818
+
2819
+ // Add letter to subtitles
2820
+ if ( onsubtitles ) {
2821
+ markdownWord += letters[i];
2822
+ }
2823
+
2824
+ // Add letter to spoken word
2825
+ if ( isSpeakable ) {
2826
+ if ( !excludes || excludes.every( x => (i < x[0]) || (i > x[1]) ) ) {
2827
+ textWord += letters[i];
2828
+ }
2829
+ }
2830
+
2831
+ // Add words to sentence and animations
2832
+ if ( isEndOfWord || isEndOfSentence || isLast ) {
2833
+
2834
+ // Add to text-to-speech sentence
2835
+ if ( textWord.length ) {
2836
+ textWord = this.lipsyncPreProcessText(textWord, lipsyncLang);
2837
+ if ( textWord.length ) {
2838
+ ttsSentence.push( {
2839
+ mark: markId,
2840
+ word: textWord
2841
+ });
2842
+ }
2843
+ }
2844
+
2845
+ // Push subtitles to animation queue
2846
+ if ( markdownWord.length ) {
2847
+ lipsyncAnim.push( {
2848
+ mark: markId,
2849
+ template: { name: 'subtitles' },
2850
+ ts: [0],
2851
+ vs: {
2852
+ subtitles: [markdownWord]
2853
+ },
2854
+ });
2855
+ markdownWord = '';
2856
+ }
2857
+
2858
+ // Push visemes to animation queue
2859
+ if ( textWord.length ) {
2860
+ const val = this.lipsyncWordsToVisemes(textWord, lipsyncLang);
2861
+ if ( val && val.visemes && val.visemes.length ) {
2862
+ const d = val.times[ val.visemes.length-1 ] + val.durations[ val.visemes.length-1 ];
2863
+ for( let j=0; j<val.visemes.length; j++ ) {
2864
+ const o =
2865
+ lipsyncAnim.push( {
2866
+ mark: markId,
2867
+ template: { name: 'viseme' },
2868
+ ts: [ (val.times[j] - 0.6) / d, (val.times[j] + 0.5) / d, (val.times[j] + val.durations[j] + 0.5) / d ],
2869
+ vs: {
2870
+ ['viseme_'+val.visemes[j]]: [null,(val.visemes[j] === 'PP' || val.visemes[j] === 'FF') ? 0.9 : 0.6,0]
2871
+ }
2872
+ });
2873
+ }
2874
+ }
2875
+ textWord = '';
2876
+ markId++;
2877
+ }
2878
+ }
2879
+
2880
+ // Process sentences
2881
+ if ( isEndOfSentence || isLast ) {
2882
+
2883
+ // Send sentence to Text-to-speech queue
2884
+ if ( ttsSentence.length || (isLast && lipsyncAnim.length) ) {
2885
+ const o = {
2886
+ anim: lipsyncAnim
2887
+ };
2888
+ if ( onsubtitles ) o.onSubtitles = onsubtitles;
2889
+ if ( ttsSentence.length && !opt.avatarMute ) {
2890
+ o.text = ttsSentence;
2891
+ if ( opt.avatarMood ) o.mood = opt.avatarMood;
2892
+ if ( opt.ttsLang ) o.lang = opt.ttsLang;
2893
+ if ( opt.ttsVoice ) o.voice = opt.ttsVoice;
2894
+ if ( opt.ttsRate ) o.rate = opt.ttsRate;
2895
+ if ( opt.ttsVoice ) o.pitch = opt.ttsPitch;
2896
+ if ( opt.ttsVolume ) o.volume = opt.ttsVolume;
2897
+ }
2898
+ this.speechQueue.push(o);
2899
+
2900
+ // Reset sentence and animation sequence
2901
+ ttsSentence = [];
2902
+ textWord = '';
2903
+ markId = 0;
2904
+ lipsyncAnim = [];
2905
+ }
2906
+
2907
+ // Send emoji, if the divider was a known emoji
2908
+ if ( isEmoji ) {
2909
+ let emoji = this.animEmojis[letters[i]];
2910
+ if ( emoji && emoji.link ) emoji = this.animEmojis[emoji.link];
2911
+ if ( emoji ) {
2912
+ this.speechQueue.push( { emoji: emoji } );
2913
+ }
2914
+ }
2915
+
2916
+ this.speechQueue.push( { break: 100 } );
2917
+
2918
+ }
2919
+
2920
+ }
2921
+
2922
+ this.speechQueue.push( { break: 1000 } );
2923
+
2924
+ // Start speaking (if not already)
2925
+ this.startSpeaking();
2926
+ }
2927
+
2928
+ /**
2929
+ * Add emoji to speech queue.
2930
+ * @param {string} em Emoji.
2931
+ */
2932
+ async speakEmoji(em) {
2933
+ let emoji = this.animEmojis[em];
2934
+ if ( emoji && emoji.link ) emoji = this.animEmojis[emoji.link];
2935
+ if ( emoji ) {
2936
+ this.speechQueue.push( { emoji: emoji } );
2937
+ }
2938
+ this.startSpeaking();
2939
+ }
2940
+
2941
+ /**
2942
+ * Add a break to the speech queue.
2943
+ * @param {numeric} t Duration in milliseconds.
2944
+ */
2945
+ async speakBreak(t) {
2946
+ this.speechQueue.push( { break: t } );
2947
+ this.startSpeaking();
2948
+ }
2949
+
2950
+ /**
2951
+ * Callback when speech queue processes this marker.
2952
+ * @param {markerfn} onmarker Callback function.
2953
+ */
2954
+ async speakMarker(onmarker) {
2955
+ this.speechQueue.push( { marker: onmarker } );
2956
+ this.startSpeaking();
2957
+ }
2958
+
2959
+ /**
2960
+ * Play background audio.
2961
+ * @param {string} url URL for the audio, stop if null.
2962
+ */
2963
+ async playBackgroundAudio( url ) {
2964
+
2965
+ // Fetch audio
2966
+ let response = await fetch(url);
2967
+ let arraybuffer = await response.arrayBuffer();
2968
+
2969
+ // Play audio in a loop
2970
+ this.stopBackgroundAudio()
2971
+ this.audioBackgroundSource = this.audioCtx.createBufferSource();
2972
+ this.audioBackgroundSource.loop = true;
2973
+ this.audioBackgroundSource.buffer = await this.audioCtx.decodeAudioData(arraybuffer);
2974
+ this.audioBackgroundSource.playbackRate.value = 1 / this.animSlowdownRate;
2975
+ this.audioBackgroundSource.connect(this.audioBackgroundGainNode);
2976
+ this.audioBackgroundSource.start(0);
2977
+
2978
+ }
2979
+
2980
+ /**
2981
+ * Stop background audio.
2982
+ */
2983
+ stopBackgroundAudio() {
2984
+ try { this.audioBackgroundSource.stop(); } catch(error) {}
2985
+ this.audioBackgroundSource.disconnect();
2986
+ }
2987
+
2988
+ /**
2989
+ * Setup the convolver node based on an impulse.
2990
+ * @param {string} [url=null] URL for the impulse, dry impulse if null
2991
+ */
2992
+ async setReverb( url=null ) {
2993
+ if ( url ) {
2994
+ // load impulse response from file
2995
+ let response = await fetch(url);
2996
+ let arraybuffer = await response.arrayBuffer();
2997
+ this.audioReverbNode.buffer = await this.audioCtx.decodeAudioData(arraybuffer);
2998
+ } else {
2999
+ // dry impulse
3000
+ const samplerate = this.audioCtx.sampleRate;
3001
+ const impulse = this.audioCtx.createBuffer(2, samplerate, samplerate);
3002
+ impulse.getChannelData(0)[0] = 1;
3003
+ impulse.getChannelData(1)[0] = 1;
3004
+ this.audioReverbNode.buffer = impulse;
3005
+ }
3006
+ }
3007
+
3008
+ /**
3009
+ * Set audio gain.
3010
+ * @param {number} speech Gain for speech, if null do not change
3011
+ * @param {number} [background=null] Gain for background audio, if null do not change
3012
+ * @param {number} [fadeSecs=0] Gradual exponential fade in/out time in seconds
3013
+ */
3014
+ setMixerGain( speech, background=null, fadeSecs=0 ) {
3015
+ if ( speech !== null ) {
3016
+ this.audioSpeechGainNode.gain.cancelScheduledValues(this.audioCtx.currentTime);
3017
+ if ( fadeSecs ) {
3018
+ this.audioSpeechGainNode.gain.setValueAtTime( Math.max( this.audioSpeechGainNode.gain.value, 0.0001), this.audioCtx.currentTime);
3019
+ this.audioSpeechGainNode.gain.exponentialRampToValueAtTime( Math.max( speech, 0.0001), this.audioCtx.currentTime + fadeSecs );
3020
+ } else {
3021
+ this.audioSpeechGainNode.gain.setValueAtTime( speech, this.audioCtx.currentTime);
3022
+ }
3023
+ }
3024
+ if ( background !== null ) {
3025
+ this.audioBackgroundGainNode.gain.cancelScheduledValues(this.audioCtx.currentTime);
3026
+ if ( fadeSecs ) {
3027
+ this.audioBackgroundGainNode.gain.setValueAtTime( Math.max( this.audioBackgroundGainNode.gain.value, 0.0001), this.audioCtx.currentTime);
3028
+ this.audioBackgroundGainNode.gain.exponentialRampToValueAtTime( Math.max( background, 0.0001 ), this.audioCtx.currentTime + fadeSecs );
3029
+ } else {
3030
+ this.audioBackgroundGainNode.gain.setValueAtTime( background, this.audioCtx.currentTime);
3031
+ }
3032
+ }
3033
+ }
3034
+
3035
+ /**
3036
+ * Add audio to the speech queue.
3037
+ * @param {Audio} r Audio message.
3038
+ * @param {Options} [opt=null] Text-specific options for lipsyncLang
3039
+ * @param {subtitlesfn} [onsubtitles=null] Callback when a subtitle is written
3040
+ */
3041
+ speakAudio(r, opt = null, onsubtitles = null ) {
3042
+ opt = opt || {};
3043
+ const lipsyncLang = opt.lipsyncLang || this.avatar.lipsyncLang || this.opt.lipsyncLang;
3044
+ const o = {};
3045
+
3046
+
3047
+ if ( r.words ) {
3048
+ let lipsyncAnim = [];
3049
+ for( let i=0; i<r.words.length; i++ ) {
3050
+ const word = r.words[i];
3051
+ const time = r.wtimes[i];
3052
+ let duration = r.wdurations[i];
3053
+
3054
+ if ( word.length ) {
3055
+
3056
+ // Subtitle
3057
+ if ( onsubtitles ) {
3058
+ lipsyncAnim.push( {
3059
+ template: { name: 'subtitles' },
3060
+ ts: [time],
3061
+ vs: {
3062
+ subtitles: [' ' + word]
3063
+ }
3064
+ });
3065
+ }
3066
+
3067
+ // If visemes were not specified, calculate visemes based on the words
3068
+ if ( !r.visemes ) {
3069
+ const wrd = this.lipsyncPreProcessText(word, lipsyncLang);
3070
+ const val = this.lipsyncWordsToVisemes(wrd, lipsyncLang);
3071
+ if ( val && val.visemes && val.visemes.length ) {
3072
+ const dTotal = val.times[ val.visemes.length-1 ] + val.durations[ val.visemes.length-1 ];
3073
+ const overdrive = Math.min(duration, Math.max( 0, duration - val.visemes.length * 150));
3074
+ let level = 0.6 + this.convertRange( overdrive, [0,duration], [0,0.4]);
3075
+ duration = Math.min( duration, val.visemes.length * 200 );
3076
+ if ( dTotal > 0 ) {
3077
+ for( let j=0; j<val.visemes.length; j++ ) {
3078
+ const t = time + (val.times[j]/dTotal) * duration;
3079
+ const d = (val.durations[j]/dTotal) * duration;
3080
+ lipsyncAnim.push( {
3081
+ template: { name: 'viseme' },
3082
+ ts: [ t - Math.min(60,2*d/3), t + Math.min(25,d/2), t + d + Math.min(60,d/2) ],
3083
+ vs: {
3084
+ ['viseme_'+val.visemes[j]]: [null,(val.visemes[j] === 'PP' || val.visemes[j] === 'FF') ? 0.9 : level, 0]
3085
+ }
3086
+ });
3087
+ }
3088
+ }
3089
+ }
3090
+ }
3091
+ }
3092
+ }
3093
+
3094
+ // If visemes were specified, use them
3095
+ if ( r.visemes ) {
3096
+ for( let i=0; i<r.visemes.length; i++ ) {
3097
+ const viseme = r.visemes[i];
3098
+ const time = r.vtimes[i];
3099
+ const duration = r.vdurations[i];
3100
+ lipsyncAnim.push( {
3101
+ template: { name: 'viseme' },
3102
+ ts: [ time - 2 * duration/3, time + duration/2, time + duration + duration/2 ],
3103
+ vs: {
3104
+ ['viseme_'+viseme]: [null,(viseme === 'PP' || viseme === 'FF') ? 0.9 : 0.6, 0]
3105
+ }
3106
+ });
3107
+ }
3108
+ }
3109
+
3110
+ // Timed marker callbacks
3111
+ if ( r.markers ) {
3112
+ for( let i=0; i<r.markers.length; i++ ) {
3113
+ const fn = r.markers[i];
3114
+ const time = r.mtimes[i];
3115
+ lipsyncAnim.push( {
3116
+ template: { name: 'markers' },
3117
+ ts: [ time ],
3118
+ vs: { "function": [fn] }
3119
+ });
3120
+ }
3121
+ }
3122
+
3123
+ if ( lipsyncAnim.length ) {
3124
+ o.anim = lipsyncAnim;
3125
+ }
3126
+
3127
+ }
3128
+
3129
+ if ( r.audio ) {
3130
+ o.audio = r.audio;
3131
+ }
3132
+
3133
+ // Blend shapes animation
3134
+ if (r.anim?.name) {
3135
+ let animObj = this.animFactory(r.anim, false, 1, 1, true);
3136
+ if (!o.anim) {
3137
+ o.anim = [ animObj ];
3138
+ } else {
3139
+ o.anim.push(animObj);
3140
+ }
3141
+ }
3142
+
3143
+ if ( onsubtitles ) {
3144
+ o.onSubtitles = onsubtitles;
3145
+ }
3146
+
3147
+ if ( opt.isRaw ) {
3148
+ o.isRaw = true;
3149
+ }
3150
+
3151
+ if ( Object.keys(o).length ) {
3152
+ this.speechQueue.push(o);
3153
+ if ( !o.isRaw ) {
3154
+ this.speechQueue.push( { break: 300 } );
3155
+ }
3156
+ this.startSpeaking();
3157
+ }
3158
+
3159
+ }
3160
+
3161
+ /**
3162
+ * Play audio playlist using Web Audio API.
3163
+ * @param {boolean} [force=false] If true, forces to proceed
3164
+ */
3165
+ async playAudio(force=false) {
3166
+ if ( !this.armature || (this.isAudioPlaying && !force) ) return;
3167
+ this.isAudioPlaying = true;
3168
+ if ( this.audioPlaylist.length ) {
3169
+ const item = this.audioPlaylist.shift();
3170
+
3171
+ // If Web Audio API is suspended, try to resume it
3172
+ if ( this.audioCtx.state === "suspended" || this.audioCtx.state === "interrupted" ) {
3173
+ const resume = this.audioCtx.resume();
3174
+ const timeout = new Promise((_r, rej) => setTimeout(() => rej("p2"), 1000));
3175
+ try {
3176
+ await Promise.race([resume, timeout]);
3177
+ } catch(e) {
3178
+ console.log("Can't play audio. Web Audio API suspended. This is often due to calling some speak method before the first user action, which is typically prevented by the browser.");
3179
+ this.playAudio(true);
3180
+ return;
3181
+ }
3182
+ }
3183
+
3184
+ // AudioBuffer
3185
+ let audio;
3186
+ if ( Array.isArray(item.audio) ) {
3187
+ // Convert from PCM samples
3188
+ let buf = this.concatArrayBuffers( item.audio );
3189
+ audio = this.pcmToAudioBuffer(buf);
3190
+ } else {
3191
+ audio = item.audio;
3192
+ }
3193
+
3194
+ // Make sure previous audio source is cleared
3195
+ if (this.audioSpeechSource) {
3196
+ try { this.audioSpeechSource.stop?.() } catch(error) {};
3197
+ this.audioSpeechSource.disconnect();
3198
+ this.audioSpeechSource.onended = null;
3199
+ this.audioSpeechSource = null;
3200
+ }
3201
+
3202
+ // Create audio source
3203
+ const source = this.audioCtx.createBufferSource();
3204
+ this.audioSpeechSource = source;
3205
+ source.buffer = audio;
3206
+ source.playbackRate.value = 1 / this.animSlowdownRate;
3207
+ source.connect(this.audioAnalyzerNode);
3208
+ source.onended = () => {
3209
+ source.disconnect();
3210
+ source.onended = null;
3211
+ if ( this.audioSpeechSource === source ) {
3212
+ this.audioSpeechSource = null;
3213
+ }
3214
+ this.playAudio(true);
3215
+ };
3216
+
3217
+ // Rescale lipsync and push to queue
3218
+ let delay = 0;
3219
+ if ( item.anim ) {
3220
+ // Find the lowest negative time point, if any
3221
+ if ( !item.isRaw ) {
3222
+ delay = Math.abs(Math.min(0, ...item.anim.map( x => Math.min(...x.ts) ) ) );
3223
+ }
3224
+ item.anim.forEach( x => {
3225
+ for(let i=0; i<x.ts.length; i++) {
3226
+ x.ts[i] = this.animClock + x.ts[i] + delay;
3227
+ }
3228
+ this.animQueue.push(x);
3229
+ });
3230
+ }
3231
+
3232
+ // Play, delay in seconds so pre-animations can be played
3233
+ source.start( this.audioCtx.currentTime + delay/1000);
3234
+
3235
+ } else {
3236
+ this.isAudioPlaying = false;
3237
+ this.startSpeaking(true);
3238
+ }
3239
+ }
3240
+
3241
+ /**
3242
+ * Take the next queue item from the speech queue, convert it to text, and
3243
+ * load the audio file.
3244
+ * @param {boolean} [force=false] If true, forces to proceed (e.g. after break)
3245
+ */
3246
+ async startSpeaking( force = false ) {
3247
+ if ( !this.armature || (this.isSpeaking && !force) ) return;
3248
+ this.stateName = 'speaking';
3249
+ this.isSpeaking = true;
3250
+ if ( this.speechQueue.length ) {
3251
+ let line = this.speechQueue.shift();
3252
+ if ( line.emoji ) {
3253
+
3254
+ // Look at the camera
3255
+ this.lookAtCamera(500);
3256
+
3257
+ // Only emoji
3258
+ let duration = line.emoji.dt.reduce((a,b) => a+b,0);
3259
+ this.animQueue.push( this.animFactory( line.emoji ) );
3260
+ setTimeout( this.startSpeaking.bind(this), duration, true );
3261
+ } else if ( line.break ) {
3262
+ // Break
3263
+ setTimeout( this.startSpeaking.bind(this), line.break, true );
3264
+ } else if ( line.audio ) {
3265
+
3266
+ // Look at the camera
3267
+ if ( !line.isRaw ) {
3268
+ this.lookAtCamera(500);
3269
+ this.speakWithHands();
3270
+ this.resetLips();
3271
+ }
3272
+
3273
+ // Make a playlist
3274
+ this.audioPlaylist.push({ anim: line.anim, audio: line.audio, isRaw: line.isRaw });
3275
+ this.onSubtitles = line.onSubtitles || null;
3276
+ if ( line.mood ) this.setMood( line.mood );
3277
+ this.playAudio();
3278
+
3279
+ } else if ( line.text ) {
3280
+
3281
+ // Look at the camera
3282
+ this.lookAtCamera(500);
3283
+
3284
+ // Spoken text
3285
+ try {
3286
+ // Convert text to SSML
3287
+ let ssml = "<speak>";
3288
+ line.text.forEach( (x,i) => {
3289
+ // Add mark
3290
+ if (i > 0) {
3291
+ ssml += " <mark name='" + x.mark + "'/>";
3292
+ }
3293
+
3294
+ // Add word
3295
+ ssml += x.word.replaceAll('&','&amp;')
3296
+ .replaceAll('<','&lt;')
3297
+ .replaceAll('>','&gt;')
3298
+ .replaceAll('"','&quot;')
3299
+ .replaceAll('\'','&apos;')
3300
+ .replace(/^\p{Dash_Punctuation}$/ug,'<break time="750ms"/>');
3301
+
3302
+ });
3303
+ ssml += "</speak>";
3304
+
3305
+
3306
+ const o = {
3307
+ method: "POST",
3308
+ headers: {
3309
+ "Content-Type": "application/json; charset=utf-8"
3310
+ },
3311
+ body: JSON.stringify({
3312
+ "input": {
3313
+ "ssml": ssml
3314
+ },
3315
+ "voice": {
3316
+ "languageCode": line.lang || this.avatar.ttsLang || this.opt.ttsLang,
3317
+ "name": line.voice || this.avatar.ttsVoice || this.opt.ttsVoice
3318
+ },
3319
+ "audioConfig": {
3320
+ "audioEncoding": this.ttsAudioEncoding,
3321
+ "speakingRate": (line.rate || this.avatar.ttsRate || this.opt.ttsRate) + this.mood.speech.deltaRate,
3322
+ "pitch": (line.pitch || this.avatar.ttsPitch || this.opt.ttsPitch) + this.mood.speech.deltaPitch,
3323
+ "volumeGainDb": (line.volume || this.avatar.ttsVolume || this.opt.ttsVolume) + this.mood.speech.deltaVolume
3324
+ },
3325
+ "enableTimePointing": [ 1 ] // Timepoint information for mark tags
3326
+ })
3327
+ };
3328
+
3329
+ // JSON Web Token
3330
+ if ( this.opt.jwtGet && typeof this.opt.jwtGet === "function" ) {
3331
+ o.headers["Authorization"] = "Bearer " + await this.opt.jwtGet();
3332
+ }
3333
+
3334
+ const res = await fetch( this.opt.ttsEndpoint + (this.opt.ttsApikey ? "?key=" + this.opt.ttsApikey : ''), o);
3335
+ const data = await res.json();
3336
+
3337
+ if ( res.status === 200 && data && data.audioContent ) {
3338
+
3339
+ // Audio data
3340
+ const buf = this.b64ToArrayBuffer(data.audioContent);
3341
+ const audio = await this.audioCtx.decodeAudioData( buf );
3342
+ this.speakWithHands();
3343
+
3344
+ // Workaround for Google TTS not providing all timepoints
3345
+ const times = [ 0 ];
3346
+ let markIndex = 0;
3347
+ line.text.forEach( (x,i) => {
3348
+ if ( i > 0 ) {
3349
+ let ms = times[ times.length - 1 ];
3350
+ if ( data.timepoints[markIndex] ) {
3351
+ ms = data.timepoints[markIndex].timeSeconds * 1000;
3352
+ if ( data.timepoints[markIndex].markName === ""+x.mark ) {
3353
+ markIndex++;
3354
+ }
3355
+ }
3356
+ times.push( ms );
3357
+ }
3358
+ });
3359
+
3360
+ // Word-to-audio alignment
3361
+ const timepoints = [ { mark: 0, time: 0 } ];
3362
+ times.forEach( (x,i) => {
3363
+ if ( i>0 ) {
3364
+ let prevDuration = x - times[i-1];
3365
+ if ( prevDuration > 150 ) prevDuration - 150; // Trim out leading space
3366
+ timepoints[i-1].duration = prevDuration;
3367
+ timepoints.push( { mark: i, time: x });
3368
+ }
3369
+ });
3370
+ let d = 1000 * audio.duration; // Duration in ms
3371
+ if ( d > this.opt.ttsTrimEnd ) d = d - this.opt.ttsTrimEnd; // Trim out silence at the end
3372
+ timepoints[timepoints.length-1].duration = d - timepoints[timepoints.length-1].time;
3373
+
3374
+ // Re-set animation starting times and rescale durations
3375
+ line.anim.forEach( x => {
3376
+ const timepoint = timepoints[x.mark];
3377
+ if ( timepoint ) {
3378
+ for(let i=0; i<x.ts.length; i++) {
3379
+ x.ts[i] = timepoint.time + (x.ts[i] * timepoint.duration) + this.opt.ttsTrimStart;
3380
+ }
3381
+ }
3382
+ });
3383
+
3384
+ // Add to the playlist
3385
+ this.audioPlaylist.push({ anim: line.anim, audio: audio });
3386
+ this.onSubtitles = line.onSubtitles || null;
3387
+ this.resetLips();
3388
+ if ( line.mood ) this.setMood( line.mood );
3389
+ this.playAudio();
3390
+
3391
+ } else {
3392
+ this.startSpeaking(true);
3393
+ }
3394
+ } catch (error) {
3395
+ console.error("Error:", error);
3396
+ this.startSpeaking(true);
3397
+ }
3398
+ } else if ( line.anim ) {
3399
+ // Only subtitles
3400
+ this.onSubtitles = line.onSubtitles || null;
3401
+ this.resetLips();
3402
+ if ( line.mood ) this.setMood( line.mood );
3403
+ line.anim.forEach( (x,i) => {
3404
+ for(let j=0; j<x.ts.length; j++) {
3405
+ x.ts[j] = this.animClock + 10 * i;
3406
+ }
3407
+ this.animQueue.push(x);
3408
+ });
3409
+ setTimeout( this.startSpeaking.bind(this), 10 * line.anim.length, true );
3410
+ } else if ( line.marker ) {
3411
+ if ( typeof line.marker === "function" ) {
3412
+ line.marker();
3413
+ }
3414
+ this.startSpeaking(true);
3415
+ } else {
3416
+ this.startSpeaking(true);
3417
+ }
3418
+ } else {
3419
+ this.stateName = 'idle';
3420
+ this.isSpeaking = false;
3421
+ }
3422
+ }
3423
+
3424
+ /**
3425
+ * Pause speaking.
3426
+ */
3427
+ pauseSpeaking() {
3428
+ try { this.audioSpeechSource?.stop(); } catch(error) {}
3429
+ this.audioPlaylist.length = 0;
3430
+ this.stateName = 'idle';
3431
+ this.isSpeaking = false;
3432
+ this.isAudioPlaying = false;
3433
+ this.animQueue = this.animQueue.filter( x => x.template.name !== 'viseme' && x.template.name !== 'subtitles' && x.template.name !== 'blendshapes' );
3434
+ if ( this.armature ) {
3435
+ this.resetLips();
3436
+ this.render();
3437
+ }
3438
+ }
3439
+
3440
+ /**
3441
+ * Stop speaking and clear the speech queue.
3442
+ */
3443
+ stopSpeaking() {
3444
+ try { this.audioSpeechSource?.stop(); } catch(error) {}
3445
+ this.audioPlaylist.length = 0;
3446
+ this.speechQueue.length = 0;
3447
+ this.animQueue = this.animQueue.filter( x => x.template.name !== 'viseme' && x.template.name !== 'subtitles' && x.template.name !== 'blendshapes' );
3448
+ this.stateName = 'idle';
3449
+ this.isSpeaking = false;
3450
+ this.isAudioPlaying = false;
3451
+ if ( this.armature ) {
3452
+ this.resetLips();
3453
+ this.render();
3454
+ }
3455
+ }
3456
+
3457
+ /**
3458
+ * Start streaming mode.
3459
+ * @param {Object} [opt={}] Optional settings include gain, sampleRate, lipsyncLang, lipsyncType, metrics, and waitForAudioChunks
3460
+ * @param {function} [onAudioStart=null] Optional callback when audio playback starts
3461
+ * @param {function} [onAudioEnd=null] Optional callback when audio streaming is automatically ended
3462
+ * @param {function} [onSubtitles=null] Optional callback to play subtitles
3463
+ * @param {function} [onMetrics=null] Optional callback to receive metrics data during streaming
3464
+ */
3465
+ async streamStart(opt = {}, onAudioStart = null, onAudioEnd = null, onSubtitles = null, onMetrics = null) {
3466
+ this.stopSpeaking(); // Stop the speech queue mode
3467
+
3468
+ this.isStreaming = true;
3469
+ if (opt.waitForAudioChunks !== undefined) {
3470
+ this.streamWaitForAudioChunks = opt.waitForAudioChunks;
3471
+ }
3472
+ if (!this.streamWaitForAudioChunks) { this.streamAudioStartTime = this.animClock; }
3473
+ this.streamLipsyncQueue = [];
3474
+ this.streamLipsyncType = opt.lipsyncType || this.streamLipsyncType || 'visemes';
3475
+ this.streamLipsyncLang = opt.lipsyncLang || this.streamLipsyncLang || this.avatar.lipsyncLang || this.opt.lipsyncLang;
3476
+ // Store callbacks for this streaming session
3477
+ this.onAudioStart = onAudioStart;
3478
+ this.onAudioEnd = onAudioEnd;
3479
+ this.onMetrics = onMetrics;
3480
+
3481
+ if (opt.sampleRate !== undefined) {
3482
+ const sr = opt.sampleRate;
3483
+ if (
3484
+ typeof sr === 'number' &&
3485
+ sr >= 8000 &&
3486
+ sr <= 96000
3487
+ ) {
3488
+ if (sr !== this.audioCtx.sampleRate) {
3489
+ this.initAudioGraph(sr);
3490
+ }
3491
+ } else {
3492
+ console.warn(
3493
+ 'Invalid sampleRate provided. It must be a number between 8000 and 96000 Hz.'
3494
+ );
3495
+ }
3496
+ }
3497
+
3498
+ if (opt.gain !== undefined) {
3499
+ this.audioStreamGainNode.gain.value = opt.gain;
3500
+ }
3501
+
3502
+ // Check if we need to create or recreate the worklet
3503
+ const needsWorkletSetup = !this.streamWorkletNode ||
3504
+ !this.streamWorkletNode.port ||
3505
+ this.streamWorkletNode.numberOfOutputs === 0 ||
3506
+ this.streamWorkletNode.context !== this.audioCtx;
3507
+
3508
+ if (needsWorkletSetup) {
3509
+ // Clean up existing worklet if it exists but is invalid
3510
+ if (this.streamWorkletNode) {
3511
+ try {
3512
+ this.streamWorkletNode.disconnect();
3513
+ this.streamWorkletNode = null;
3514
+ } catch (e) {
3515
+ // Ignore errors during cleanup
3516
+ }
3517
+ }
3518
+
3519
+ if (!this.workletLoaded) {
3520
+ try {
3521
+ const loadPromise = this.audioCtx.audioWorklet.addModule(workletUrl.href);
3522
+ const timeoutPromise = new Promise((_, reject) =>
3523
+ setTimeout(() => reject(new Error("Worklet loading timed out")), 5000)
3524
+ );
3525
+ await Promise.race([loadPromise, timeoutPromise]);
3526
+ this.workletLoaded = true;
3527
+ } catch (error) {
3528
+ console.error("Failed to load audio worklet:", error);
3529
+ throw new Error("Failed to initialize streaming speech");
3530
+ }
3531
+ }
3532
+
3533
+ this.streamWorkletNode = new AudioWorkletNode(this.audioCtx, 'playback-worklet', {
3534
+ processorOptions: {
3535
+ sampleRate: this.audioCtx.sampleRate,
3536
+ metrics: opt.metrics || { enabled: false }
3537
+ }
3538
+ });
3539
+
3540
+ // Connect the node to the audio graph
3541
+ this.streamWorkletNode.connect(this.audioStreamGainNode);
3542
+ this.streamWorkletNode.connect(this.audioAnalyzerNode);
3543
+
3544
+ this.streamWorkletNode.port.onmessage = (event) => {
3545
+
3546
+ if(event.data.type === 'playback-started') {
3547
+ this.isSpeaking = true;
3548
+ this.stateName = "speaking";
3549
+ if (this.streamWaitForAudioChunks) this.streamAudioStartTime = this.animClock;
3550
+ this._processStreamLipsyncQueue();
3551
+ this.speakWithHands();
3552
+ if (this.onAudioStart) {
3553
+ try { this.onAudioStart?.(); } catch(e) { console.error(e); }
3554
+ }
3555
+ }
3556
+
3557
+ if (event.data.type === 'playback-ended') {
3558
+ this._streamPause();
3559
+ if (this.onAudioEnd) {
3560
+ try { this.onAudioEnd(); } catch(e) {}
3561
+ }
3562
+ }
3563
+
3564
+ // Forward diagnostic metrics if provided
3565
+ if (this.onMetrics && event.data.type === 'metrics') {
3566
+ try { this.onMetrics(event.data); } catch(e) { /* ignore */ }
3567
+ }
3568
+ };
3569
+ }
3570
+
3571
+ // Update metrics config if provided (can be different per session)
3572
+ if (opt.metrics) {
3573
+ try { this.streamWorkletNode.port.postMessage({ type: 'config-metrics', data: opt.metrics }); } catch(e) {}
3574
+ }
3575
+
3576
+ this.resetLips();
3577
+ this.lookAtCamera(500);
3578
+ opt.mood && this.setMood( opt.mood );
3579
+ this.onSubtitles = onSubtitles || null;
3580
+
3581
+ // If Web Audio API is suspended, try to resume it
3582
+ if ( this.audioCtx.state === "suspended" || this.audioCtx.state === "interrupted" ) {
3583
+ const resume = this.audioCtx.resume();
3584
+ const timeout = new Promise((_r, rej) => setTimeout(() => rej("p2"), 1000));
3585
+ try {
3586
+ await Promise.race([resume, timeout]);
3587
+ } catch(e) {
3588
+ console.warn("Can't play audio. Web Audio API suspended. This is often due to calling some speak method before the first user action, which is typically prevented by the browser.");
3589
+ return;
3590
+ }
3591
+ }
3592
+ }
3593
+
3594
+ /**
3595
+ * Notify if no more streaming data is coming.
3596
+ * Actual stop occurs after audio playback.
3597
+ */
3598
+ streamNotifyEnd() {
3599
+ if (!this.isStreaming || !this.streamWorkletNode) return;
3600
+
3601
+ this.streamWorkletNode.port.postMessage({ type: 'no-more-data' });
3602
+ }
3603
+
3604
+ /**
3605
+ * Interrupt ongoing stream audio and lipsync
3606
+ */
3607
+ streamInterrupt() {
3608
+ if (!this.isStreaming) return;
3609
+ const wasSpeaking = this.isSpeaking;
3610
+
3611
+ if (this.streamWorkletNode) {
3612
+ try { this.streamWorkletNode.port.postMessage({type: 'stop'}); } catch(e) {}
3613
+ }
3614
+ this._streamPause(true);
3615
+
3616
+ if (wasSpeaking && this.onAudioEnd) {
3617
+ try { this.onAudioEnd(); } catch(e) {}
3618
+ }
3619
+ }
3620
+
3621
+ /**
3622
+ * Stop streaming mode
3623
+ */
3624
+ streamStop() {
3625
+ if (!this.isStreaming) return;
3626
+ this.streamInterrupt();
3627
+ if (this.streamWorkletNode) {
3628
+ try {
3629
+ this.streamWorkletNode.disconnect();
3630
+ } catch(e) { /* ignore */ }
3631
+ this.streamWorkletNode = null;
3632
+ }
3633
+ this.isStreaming = false;
3634
+ }
3635
+
3636
+
3637
+ /**
3638
+ * Internal function to pause the speaking state after a speech utterance.
3639
+ * This is called when the audio stream ends or is interrupted.
3640
+ * @param {boolean} interrupt_lipsync - If true, interrupts the lipsync
3641
+ * @private
3642
+ */
3643
+ _streamPause(interrupt_lipsync = false) {
3644
+ this.isSpeaking = false;
3645
+ this.stateName = "idle";
3646
+
3647
+ // force stop the speech animation.
3648
+ if(interrupt_lipsync) {
3649
+ if (this.streamWaitForAudioChunks) this.streamAudioStartTime = null;
3650
+ this.streamLipsyncQueue = [];
3651
+ this.animQueue = this.animQueue.filter( x => x.template.name !== 'viseme' && x.template.name !== 'subtitles' && x.template.name !== 'blendshapes' );
3652
+ if ( this.armature ) {
3653
+ this.resetLips();
3654
+ this.render();
3655
+ }
3656
+ }
3657
+ }
3658
+
3659
+ /**
3660
+ * Processes all lipsync data items currently in the streamLipsyncQueue.
3661
+ * This is called once the actual audio start time is known.
3662
+ * @private
3663
+ */
3664
+ _processStreamLipsyncQueue() {
3665
+ if (!this.isStreaming) return;
3666
+ while (this.streamLipsyncQueue.length > 0) {
3667
+ const lipsyncPayload = this.streamLipsyncQueue.shift();
3668
+ // Pass the now confirmed streamAudioStartTime
3669
+ this._processLipsyncData(lipsyncPayload, this.streamAudioStartTime);
3670
+ }
3671
+ }
3672
+
3673
+ /**
3674
+ * Processes the lipsync data for the current audio stream.
3675
+ * * @param {Object} r The lipsync data object.
3676
+ * * @param {number} audioStart The start time of the audio stream.
3677
+ * * @private
3678
+ */
3679
+ _processLipsyncData(r, audioStart) {
3680
+ // Early return if streaming has been stopped
3681
+ if (!this.isStreaming) return;
3682
+
3683
+ // Process visemes
3684
+ if (r.visemes && this.streamLipsyncType == 'visemes') {
3685
+ for (let i = 0; i < r.visemes.length; i++) {
3686
+ const viseme = r.visemes[i];
3687
+ const time = audioStart + r.vtimes[i];
3688
+ const duration = r.vdurations[i];
3689
+ const animObj = {
3690
+ template: { name: 'viseme' },
3691
+ ts: [time - 2 * duration / 3, time + duration / 2, time + duration + duration / 2],
3692
+ vs: {
3693
+ ['viseme_' + viseme]: [null, (viseme === 'PP' || viseme === 'FF') ? 0.9 : 0.6, 0]
3694
+ }
3695
+ }
3696
+ this.animQueue.push(animObj);
3697
+ }
3698
+ }
3699
+
3700
+ // Process words
3701
+ if (r.words && (this.onSubtitles || this.streamLipsyncType == "words")) {
3702
+ for (let i = 0; i < r.words.length; i++) {
3703
+ const word = r.words[i];
3704
+ const time = r.wtimes[i];
3705
+ let duration = r.wdurations[i];
3706
+
3707
+ if (word.length) {
3708
+ // If subtitles callback is available, add the subtitles
3709
+ if (this.onSubtitles) {
3710
+ this.animQueue.push({
3711
+ template: { name: 'subtitles' },
3712
+ ts: [audioStart + time],
3713
+ vs: {
3714
+ subtitles: [' ' + word]
3715
+ }
3716
+ });
3717
+ }
3718
+
3719
+ // Calculate visemes based on the words
3720
+ if (this.streamLipsyncType == "words") {
3721
+ const lipsyncLang = this.streamLipsyncLang || this.avatar.lipsyncLang || this.opt.lipsyncLang;
3722
+ const wrd = this.lipsyncPreProcessText(word, lipsyncLang);
3723
+ const val = this.lipsyncWordsToVisemes(wrd, lipsyncLang);
3724
+ if (val && val.visemes && val.visemes.length) {
3725
+ const dTotal = val.times[val.visemes.length - 1] + val.durations[val.visemes.length - 1];
3726
+ const overdrive = Math.min(duration, Math.max(0, duration - val.visemes.length * 150));
3727
+ let level = 0.6 + this.convertRange(overdrive, [0, duration], [0, 0.4]);
3728
+ duration = Math.min(duration, val.visemes.length * 200);
3729
+ if (dTotal > 0) {
3730
+ for (let j = 0; j < val.visemes.length; j++) {
3731
+ const t = audioStart + time + (val.times[j] / dTotal) * duration;
3732
+ const d = (val.durations[j] / dTotal) * duration;
3733
+ this.animQueue.push({
3734
+ template: { name: 'viseme' },
3735
+ ts: [t - Math.min(60, 2 * d / 3), t + Math.min(25, d / 2), t + d + Math.min(60, d / 2)],
3736
+ vs: {
3737
+ ['viseme_' + val.visemes[j]]: [null, (val.visemes[j] === 'PP' || val.visemes[j] === 'FF') ? 0.9 : level, 0]
3738
+ }
3739
+ });
3740
+ }
3741
+ }
3742
+ }
3743
+ }
3744
+ }
3745
+ }
3746
+ }
3747
+
3748
+ // If blendshapes anims are provided, add them to animQueue
3749
+ if (r.anims && this.streamLipsyncType == "blendshapes") {
3750
+ for (let i = 0; i < r.anims.length; i++) {
3751
+ let anim = r.anims[i];
3752
+ anim.delay += audioStart;
3753
+ let animObj = this.animFactory(anim, false, 1, 1, true);
3754
+ this.animQueue.push(animObj);
3755
+ }
3756
+ }
3757
+ }
3758
+
3759
+ /**
3760
+ * stream audio and lipsync. Audio must be in 16 bit PCM format.
3761
+ * @param r Audio object with viseme data.
3762
+ */
3763
+ streamAudio(r) {
3764
+ if (!this.isStreaming || !this.streamWorkletNode) return;
3765
+ if (!this.isSpeaking) {
3766
+ this.streamLipsyncQueue = [];
3767
+ this.streamAudioStartTime = null;
3768
+ }
3769
+ this.isSpeaking = true;
3770
+ this.stateName = "speaking";
3771
+
3772
+ // Process audio data if provided
3773
+ if (r.audio !== undefined) {
3774
+ const message = { type: 'audioData', data: null };
3775
+
3776
+ // Feed ArrayBuffer for performance. Other fallback formats require copy/conversion.
3777
+ if (r.audio instanceof ArrayBuffer) {
3778
+ message.data = r.audio;
3779
+ this.streamWorkletNode.port.postMessage(message, [message.data]);
3780
+ }
3781
+ else if (r.audio instanceof Int16Array || r.audio instanceof Uint8Array) {
3782
+ const bufferCopy = r.audio.buffer.slice(r.audio.byteOffset, r.audio.byteOffset + r.audio.byteLength);
3783
+ message.data = bufferCopy;
3784
+ this.streamWorkletNode.port.postMessage(message, [message.data]);
3785
+ }
3786
+ else if (r.audio instanceof Float32Array) {
3787
+ // Convert Float32 -> Int16 PCM
3788
+ const int16Buffer = new Int16Array(r.audio.length);
3789
+ for (let i = 0; i < r.audio.length; i++) {
3790
+ let s = Math.max(-1, Math.min(1, r.audio[i])); // clamp
3791
+ int16Buffer[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
3792
+ }
3793
+ message.data = int16Buffer.buffer;
3794
+ this.streamWorkletNode.port.postMessage(message, [message.data]);
3795
+ }
3796
+ else {
3797
+ console.error("r.audio is not a supported type. Must be ArrayBuffer, Int16Array, Uint8Array, or Float32Array:", r.audio);
3798
+ }
3799
+ }
3800
+
3801
+ if(r.visemes || r.anims || r.words) {
3802
+ if(this.streamWaitForAudioChunks && !this.streamAudioStartTime) {
3803
+ // Lipsync data received before audio playback start. Queue the lipsync data.
3804
+ if (this.streamLipsyncQueue.length >= 200) { // set maximum queue length
3805
+ this.streamLipsyncQueue.shift();
3806
+ }
3807
+ this.streamLipsyncQueue.push(r);
3808
+ return;
3809
+ } else if(!this.streamWaitForAudioChunks && !this.streamAudioStartTime) {
3810
+ this.streamAudioStartTime = this.animClock;
3811
+ }
3812
+ this._processLipsyncData(r, this.streamAudioStartTime);
3813
+ }
3814
+ }
3815
+
3816
+ /**
3817
+ * Make eye contact.
3818
+ * @param {number} t Time in milliseconds
3819
+ */
3820
+ makeEyeContact(t) {
3821
+ this.animQueue.push( this.animFactory( {
3822
+ name: 'eyecontact', dt: [0,t], vs: { eyeContact: [1] }
3823
+ }));
3824
+ }
3825
+
3826
+ /**
3827
+ * Look ahead.
3828
+ * @param {number} t Time in milliseconds
3829
+ */
3830
+ lookAhead(t) {
3831
+
3832
+ if ( t ) {
3833
+
3834
+ // Randomize head/eyes ratio
3835
+ let drotx = (Math.random() - 0.5) / 4;
3836
+ let droty = (Math.random() - 0.5) / 4;
3837
+
3838
+ // Remove old, if any
3839
+ let old = this.animQueue.findIndex( y => y.template.name === 'lookat' );
3840
+ if ( old !== -1 ) {
3841
+ this.animQueue.splice(old, 1);
3842
+ }
3843
+
3844
+ // Add new anim
3845
+ const templateLookAt = {
3846
+ name: 'lookat',
3847
+ dt: [750,t],
3848
+ vs: {
3849
+ bodyRotateX: [ drotx ],
3850
+ bodyRotateY: [ droty ],
3851
+ eyesRotateX: [ - 3 * drotx + 0.1 ],
3852
+ eyesRotateY: [ - 5 * droty ],
3853
+ browInnerUp: [[0,0.7]],
3854
+ mouthLeft: [[0,0.7]],
3855
+ mouthRight: [[0,0.7]],
3856
+ eyeContact: [0],
3857
+ headMove: [0]
3858
+ }
3859
+ };
3860
+ this.animQueue.push( this.animFactory( templateLookAt ) );
3861
+ }
3862
+
3863
+ }
3864
+
3865
+ /**
3866
+ * Turn head and eyes to look at the camera.
3867
+ * @param {number} t Time in milliseconds
3868
+ */
3869
+ lookAtCamera(t) {
3870
+
3871
+ // Calculate the target
3872
+ let target;
3873
+ if ( this.speakTo ) {
3874
+ target = new THREE.Vector3();
3875
+ if ( this.speakTo.objectLeftEye?.isObject3D ) {
3876
+
3877
+ // Target eyes
3878
+ const o = this.speakTo.armature.objectHead;
3879
+ this.speakTo.objectLeftEye.updateMatrixWorld(true);
3880
+ this.speakTo.objectRightEye.updateMatrixWorld(true);
3881
+ v.setFromMatrixPosition(this.speakTo.objectLeftEye.matrixWorld);
3882
+ w.setFromMatrixPosition(this.speakTo.objectRightEye.matrixWorld);
3883
+ target.addVectors(v,w).divideScalar( 2 );
3884
+
3885
+ } else if ( this.speakTo.isObject3D ) {
3886
+ this.speakTo.getWorldPosition(target);
3887
+ } else if ( this.speakTo.isVector3 ) {
3888
+ target.set( this.speakTo );
3889
+ } else if ( this.speakTo.x && this.speakTo.y && this.speakTo.z ) {
3890
+ target.set( this.speakTo.x, this.speakTo.y, this.speakTo.z );
3891
+ }
3892
+ }
3893
+
3894
+ // If we don't have a target, look ahead or to the screen
3895
+ if ( !target ) {
3896
+ if ( this.avatar.hasOwnProperty('avatarIgnoreCamera') ) {
3897
+ if ( this.avatar.avatarIgnoreCamera ) {
3898
+ this.lookAhead(t);
3899
+ return;
3900
+ }
3901
+ } else if ( this.opt.avatarIgnoreCamera ) {
3902
+ this.lookAhead(t);
3903
+ return;
3904
+ }
3905
+ this.lookAt(null,null,t);
3906
+ return;
3907
+ }
3908
+
3909
+ // TODO: Improve the logic, if possible
3910
+
3911
+ // Eyes position and head world rotation
3912
+ this.objectLeftEye.updateMatrixWorld(true);
3913
+ this.objectRightEye.updateMatrixWorld(true);
3914
+ v.setFromMatrixPosition(this.objectLeftEye.matrixWorld);
3915
+ w.setFromMatrixPosition(this.objectRightEye.matrixWorld);
3916
+ v.add(w).divideScalar( 2 );
3917
+ q.copy( this.armature.quaternion );
3918
+ q.multiply( this.poseTarget.props['Hips.quaternion'] );
3919
+ q.multiply( this.poseTarget.props['Spine.quaternion'] );
3920
+ q.multiply( this.poseTarget.props['Spine1.quaternion'] );
3921
+ q.multiply( this.poseTarget.props['Spine2.quaternion'] );
3922
+ q.multiply( this.poseTarget.props['Neck.quaternion'] );
3923
+ q.multiply( this.poseTarget.props['Head.quaternion'] );
3924
+
3925
+ // Direction from object to speakto target
3926
+ const dir = new THREE.Vector3().subVectors(target, v).normalize();
3927
+
3928
+ // Remove roll: compute yaw + pitch only
3929
+ const yaw = Math.atan2(dir.x, dir.z); // rotation around Y
3930
+ const pitch = Math.asin(-dir.y); // rotation around X
3931
+ const roll = 0; // force to 0
3932
+
3933
+ // Desired rotation with Z locked
3934
+ e.set(pitch, yaw, roll, 'YXZ');
3935
+ const desiredQ = new THREE.Quaternion().setFromEuler(e);
3936
+
3937
+ // Rotation difference
3938
+ const deltaQ = new THREE.Quaternion().copy(desiredQ).multiply(q.clone().invert());
3939
+
3940
+ // Convert to Euler (Z will be ~0 by construction)
3941
+ e.setFromQuaternion(deltaQ, 'YXZ');
3942
+ let rx = e.x / (40/24) + 0.2; // Refer to setValue(bodyRotateX)
3943
+ let ry = e.y / (9/4); // Refer to setValue(bodyRotateY)
3944
+ let rotx = Math.min(0.6,Math.max(-0.3,rx));
3945
+ let roty = Math.min(0.8,Math.max(-0.8,ry));
3946
+
3947
+ // Randomize head/eyes ratio
3948
+ let drotx = (Math.random() - 0.5) / 4;
3949
+ let droty = (Math.random() - 0.5) / 4;
3950
+
3951
+ if ( t ) {
3952
+
3953
+ // Remove old, if any
3954
+ let old = this.animQueue.findIndex( y => y.template.name === 'lookat' );
3955
+ if ( old !== -1 ) {
3956
+ this.animQueue.splice(old, 1);
3957
+ }
3958
+
3959
+ // Add new anim
3960
+ const templateLookAt = {
3961
+ name: 'lookat',
3962
+ dt: [750,t],
3963
+ vs: {
3964
+ bodyRotateX: [ rotx + drotx ],
3965
+ bodyRotateY: [ roty + droty ],
3966
+ eyesRotateX: [ - 3 * drotx + 0.1 ],
3967
+ eyesRotateY: [ - 5 * droty ],
3968
+ browInnerUp: [[0,0.7]],
3969
+ mouthLeft: [[0,0.7]],
3970
+ mouthRight: [[0,0.7]],
3971
+ eyeContact: [0],
3972
+ headMove: [0]
3973
+ }
3974
+ };
3975
+ this.animQueue.push( this.animFactory( templateLookAt ) );
3976
+ }
3977
+
3978
+ }
3979
+
3980
+ /**
3981
+ * Turn head and eyes to look at the point (x,y).
3982
+ * @param {number} x X-coordinate relative to visual viewport
3983
+ * @param {number} y Y-coordinate relative to visual viewport
3984
+ * @param {number} t Time in milliseconds
3985
+ */
3986
+ lookAt(x,y,t) {
3987
+ if ( !this.camera ) return; // Can't be done w/o knowing the camera location
3988
+
3989
+ // Eyes position
3990
+ const rect = this.nodeAvatar.getBoundingClientRect();
3991
+ this.objectLeftEye.updateMatrixWorld(true);
3992
+ this.objectRightEye.updateMatrixWorld(true);
3993
+ const plEye = new THREE.Vector3().setFromMatrixPosition(this.objectLeftEye.matrixWorld);
3994
+ const prEye = new THREE.Vector3().setFromMatrixPosition(this.objectRightEye.matrixWorld);
3995
+ const pEyes = new THREE.Vector3().addVectors( plEye, prEye ).divideScalar( 2 );
3996
+
3997
+ pEyes.project(this.camera);
3998
+ let eyesx = (pEyes.x + 1) / 2 * rect.width + rect.left;
3999
+ let eyesy = -(pEyes.y - 1) / 2 * rect.height + rect.top;
4000
+
4001
+ // if coordinate not specified, look at the camera
4002
+ if ( x === null ) x = eyesx;
4003
+ if ( y === null ) y = eyesy;
4004
+
4005
+ // Use body/camera rotation to determine the required head rotation
4006
+ q.copy( this.armature.quaternion );
4007
+ q.multiply( this.poseTarget.props['Hips.quaternion'] );
4008
+ q.multiply( this.poseTarget.props['Spine.quaternion'] );
4009
+ q.multiply( this.poseTarget.props['Spine1.quaternion'] );
4010
+ q.multiply( this.poseTarget.props['Spine2.quaternion'] );
4011
+ q.multiply( this.poseTarget.props['Neck.quaternion'] );
4012
+ q.multiply( this.poseTarget.props['Head.quaternion'] );
4013
+ e.setFromQuaternion(q);
4014
+ let rx = e.x / (40/24); // Refer to setValue(bodyRotateX)
4015
+ let ry = e.y / (9/4); // Refer to setValue(bodyRotateY)
4016
+ let camerarx = Math.min(0.4, Math.max(-0.4,this.camera.rotation.x));
4017
+ let camerary = Math.min(0.4, Math.max(-0.4,this.camera.rotation.y));
4018
+
4019
+ // Calculate new delta
4020
+ let maxx = Math.max( window.innerWidth - eyesx, eyesx );
4021
+ let maxy = Math.max( window.innerHeight - eyesy, eyesy );
4022
+ let rotx = this.convertRange(y,[eyesy-maxy,eyesy+maxy],[-0.3,0.6]) - rx + camerarx;
4023
+ let roty = this.convertRange(x,[eyesx-maxx,eyesx+maxx],[-0.8,0.8]) - ry + camerary;
4024
+ rotx = Math.min(0.6,Math.max(-0.3,rotx));
4025
+ roty = Math.min(0.8,Math.max(-0.8,roty));
4026
+
4027
+ // Randomize head/eyes ratio
4028
+ let drotx = (Math.random() - 0.5) / 4;
4029
+ let droty = (Math.random() - 0.5) / 4;
4030
+
4031
+ if ( t ) {
4032
+
4033
+ // Remove old, if any
4034
+ let old = this.animQueue.findIndex( y => y.template.name === 'lookat' );
4035
+ if ( old !== -1 ) {
4036
+ this.animQueue.splice(old, 1);
4037
+ }
4038
+
4039
+ // Add new anim
4040
+ const templateLookAt = {
4041
+ name: 'lookat',
4042
+ dt: [750,t],
4043
+ vs: {
4044
+ bodyRotateX: [ rotx + drotx ],
4045
+ bodyRotateY: [ roty + droty ],
4046
+ eyesRotateX: [ - 3 * drotx + 0.1 ],
4047
+ eyesRotateY: [ - 5 * droty ],
4048
+ browInnerUp: [[0,0.7]],
4049
+ mouthLeft: [[0,0.7]],
4050
+ mouthRight: [[0,0.7]],
4051
+ eyeContact: [0],
4052
+ headMove: [0]
4053
+ }
4054
+ };
4055
+ this.animQueue.push( this.animFactory( templateLookAt ) );
4056
+ }
4057
+ }
4058
+
4059
+
4060
+ /**
4061
+ * Set the closest hand to touch at (x,y).
4062
+ * @param {number} x X-coordinate relative to visual viewport
4063
+ * @param {number} y Y-coordinate relative to visual viewport
4064
+ * @return {Boolean} If true, (x,y) touch the avatar
4065
+ */
4066
+ touchAt(x,y) {
4067
+ if ( !this.camera ) return; // Can't be done w/o knowing the camera location
4068
+
4069
+ const rect = this.nodeAvatar.getBoundingClientRect();
4070
+ const pointer = new THREE.Vector2(
4071
+ ( (x - rect.left) / rect.width ) * 2 - 1,
4072
+ - ( (y - rect.top) / rect.height ) * 2 + 1
4073
+ );
4074
+ const raycaster = new THREE.Raycaster();
4075
+ raycaster.setFromCamera(pointer,this.camera);
4076
+ const intersects = raycaster.intersectObject(this.armature);
4077
+ if ( intersects.length > 0 ) {
4078
+ const target = intersects[0].point;
4079
+ const LeftArmPos = new THREE.Vector3();
4080
+ const RightArmPos = new THREE.Vector3();
4081
+ this.objectLeftArm.getWorldPosition(LeftArmPos);
4082
+ this.objectRightArm.getWorldPosition(RightArmPos);
4083
+ const LeftD2 = LeftArmPos.distanceToSquared(target);
4084
+ const RightD2 = RightArmPos.distanceToSquared(target);
4085
+ if ( LeftD2 < RightD2 ) {
4086
+ this.ikSolve( {
4087
+ iterations: 20, root: "LeftShoulder", effector: "LeftHandMiddle1",
4088
+ links: [
4089
+ { link: "LeftHand", minx: -0.5, maxx: 0.5, miny: -1, maxy: 1, minz: -0.5, maxz: 0.5, maxAngle: 0.1 },
4090
+ { link: "LeftForeArm", minx: -0.5, maxx: 1.5, miny: -1.5, maxy: 1.5, minz: -0.5, maxz: 3, maxAngle: 0.2 },
4091
+ { link: "LeftArm", minx: -1.5, maxx: 1.5, miny: 0, maxy: 0, minz: -1, maxz: 3 }
4092
+ ]
4093
+ }, target, false, 1000 );
4094
+ this.setValue("handFistLeft",0);
4095
+ } else {
4096
+ this.ikSolve( {
4097
+ iterations: 20, root: "RightShoulder", effector: "RightHandMiddle1",
4098
+ links: [
4099
+ { link: "RightHand", minx: -0.5, maxx: 0.5, miny: -1, maxy: 1, minz: -0.5, maxz: 0.5, maxAngle: 0.1 },
4100
+ { link: "RightForeArm", minx: -0.5, maxx: 1.5, miny: -1.5, maxy: 1.5, minz: -3, maxz: 0.5, maxAngle: 0.2 },
4101
+ { link: "RightArm", minx: -1.5, maxx: 1.5, miny: 0, maxy: 0, minz: -1, maxz: 3 }
4102
+ ]
4103
+ }, target, false, 1000 );
4104
+ this.setValue("handFistRight",0);
4105
+ }
4106
+ } else {
4107
+ ["LeftArm","LeftForeArm","LeftHand","RightArm","RightForeArm","RightHand"].forEach( x => {
4108
+ let key = x + ".quaternion";
4109
+ this.poseTarget.props[key].copy( this.getPoseTemplateProp(key) );
4110
+ this.poseTarget.props[key].t = this.animClock;
4111
+ this.poseTarget.props[key].d = 1000;
4112
+ });
4113
+ }
4114
+
4115
+ return ( intersects.length > 0 );
4116
+ }
4117
+
4118
+ /**
4119
+ * Talk with hands.
4120
+ * @param {number} [delay=0] Delay in milliseconds
4121
+ * @param {number} [prob=1] Probability of hand movement
4122
+ */
4123
+ speakWithHands(delay=0,prob=0.5) {
4124
+
4125
+ // Only if we are standing and not bending and probabilities match up
4126
+ if ( this.mixer || this.gesture || !this.poseTarget.template.standing || this.poseTarget.template.bend || Math.random()>prob ) return;
4127
+
4128
+ // Random targets for left hand
4129
+ this.ikSolve( {
4130
+ root: "LeftShoulder", effector: "LeftHandMiddle1",
4131
+ links: [
4132
+ { link: "LeftHand", minx: -0.5, maxx: 0.5, miny: -1, maxy: 1, minz: -0.5, maxz: 0.5 },
4133
+ { link: "LeftForeArm", minx: -0.5, maxx: 1.5, miny: -1.5, maxy: 1.5, minz: -0.5, maxz: 3 },
4134
+ { link: "LeftArm", minx: -1.5, maxx: 1.5, miny: -1.5, maxy: 1.5, minz: -1, maxz: 3 }
4135
+ ]
4136
+ }, new THREE.Vector3(
4137
+ this.gaussianRandom(0,0.5),
4138
+ this.gaussianRandom(-0.8,-0.2),
4139
+ this.gaussianRandom(0,0.5)
4140
+ ), true);
4141
+
4142
+ // Random target for right hand
4143
+ this.ikSolve( {
4144
+ root: "RightShoulder", effector: "RightHandMiddle1",
4145
+ links: [
4146
+ { link: "RightHand", minx: -0.5, maxx: 0.5, miny: -1, maxy: 1, minz: -0.5, maxz: 0.5 },
4147
+ { link: "RightForeArm", minx: -0.5, maxx: 1.5, miny: -1.5, maxy: 1.5, minz: -3, maxz: 0.5 },
4148
+ { link: "RightArm" }
4149
+ ]
4150
+ }, new THREE.Vector3(
4151
+ this.gaussianRandom(-0.5,0),
4152
+ this.gaussianRandom(-0.8,-0.2),
4153
+ this.gaussianRandom(0,0.5)
4154
+ ), true);
4155
+
4156
+ // Moveto
4157
+ const dt = [];
4158
+ const moveto = [];
4159
+
4160
+ // First move
4161
+ dt.push( 100 + Math.round( Math.random() * 500 ) );
4162
+ moveto.push( { duration: 1000, props: {
4163
+ "LeftHand.quaternion": new THREE.Quaternion().setFromEuler( new THREE.Euler( 0, -1 - Math.random(), 0 ) ),
4164
+ "RightHand.quaternion": new THREE.Quaternion().setFromEuler( new THREE.Euler( 0, 1 + Math.random(), 0 ) )
4165
+ } } );
4166
+ ["LeftArm","LeftForeArm","RightArm","RightForeArm"].forEach( x => {
4167
+ moveto[0].props[x+'.quaternion'] = this.ikMesh.getObjectByName(x).quaternion.clone();
4168
+ });
4169
+
4170
+ // Return to original target
4171
+ dt.push( 1000 + Math.round( Math.random() * 500 ) );
4172
+ moveto.push( { duration: 2000, props: {} } );
4173
+ ["LeftArm","LeftForeArm","RightArm","RightForeArm","LeftHand","RightHand"].forEach( x => {
4174
+ moveto[1].props[x+'.quaternion'] = null;
4175
+ });
4176
+
4177
+ // Make an animation
4178
+ const anim = this.animFactory( {
4179
+ name: 'talkinghands',
4180
+ delay: delay,
4181
+ dt: dt,
4182
+ vs: { moveto: moveto }
4183
+ });
4184
+ this.animQueue.push( anim );
4185
+
4186
+ }
4187
+
4188
+ /**
4189
+ * Get slowdown.
4190
+ * @return {numeric} Slowdown factor.
4191
+ */
4192
+ getSlowdownRate(k) {
4193
+ return this.animSlowdownRate;
4194
+ }
4195
+
4196
+ /**
4197
+ * Set slowdown.
4198
+ * @param {numeric} k Slowdown factor.
4199
+ */
4200
+ setSlowdownRate(k) {
4201
+ this.animSlowdownRate = k;
4202
+ if ( this.audioSpeechSource ) {
4203
+ this.audioSpeechSource.playbackRate.value = 1 / this.animSlowdownRate;
4204
+ }
4205
+ if ( this.audioBackgroundSource ) {
4206
+ this.audioBackgroundSource.playbackRate.value = 1 / this.animSlowdownRate;
4207
+ }
4208
+ }
4209
+
4210
+ /**
4211
+ * Get autorotate speed.
4212
+ * @return {numeric} Autorotate speed.
4213
+ */
4214
+ getAutoRotateSpeed(k) {
4215
+ return this.controls.autoRotateSpeed;
4216
+ }
4217
+
4218
+ /**
4219
+ * Set autorotate.
4220
+ * @param {numeric} speed Autorotate speed, e.g. value 2 = 30 secs per orbit at 60fps.
4221
+ */
4222
+ setAutoRotateSpeed(speed) {
4223
+ this.controls.autoRotateSpeed = speed;
4224
+ this.controls.autoRotate = (speed > 0);
4225
+ }
4226
+
4227
+ /**
4228
+ * Start animation cycle.
4229
+ */
4230
+ start() {
4231
+ if ( this.armature && this.isRunning === false ) {
4232
+ this.audioCtx.resume();
4233
+ this.animTimeLast = performance.now();
4234
+ this.isRunning = true;
4235
+ if ( !this.isAvatarOnly ) {
4236
+ this._raf = requestAnimationFrame( this.animate );
4237
+ }
4238
+ }
4239
+ }
4240
+
4241
+ /**
4242
+ * Stop animation cycle.
4243
+ */
4244
+ stop() {
4245
+ this.isRunning = false;
4246
+ this.audioCtx.suspend();
4247
+ }
4248
+
4249
+ /**
4250
+ * Start listening incoming audio.
4251
+ * @param {AnalyserNode} analyzer Analyzer node for incoming audio
4252
+ * @param {Object} [opt={}] Options
4253
+ * @param {function} [onchange=null] Callback function for start
4254
+ */
4255
+ startListening(analyzer, opt = {}, onchange = null) {
4256
+ this.listeningAnalyzer = analyzer;
4257
+ this.listeningAnalyzer.fftSize = 256;
4258
+ this.listeningAnalyzer.smoothingTimeConstant = 0.1;
4259
+ this.listeningAnalyzer.minDecibels = -70;
4260
+ this.listeningAnalyzer.maxDecibels = -10;
4261
+ this.listeningOnchange = (onchange && typeof onchange === 'function') ? onchange : null;
4262
+
4263
+ this.listeningSilenceThresholdLevel = opt?.hasOwnProperty('listeningSilenceThresholdLevel') ? opt.listeningSilenceThresholdLevel : this.opt.listeningSilenceThresholdLevel;
4264
+ this.listeningSilenceThresholdMs = opt?.hasOwnProperty('listeningSilenceThresholdMs') ? opt.listeningSilenceThresholdMs : this.opt.listeningSilenceThresholdMs;
4265
+ this.listeningSilenceDurationMax = opt?.hasOwnProperty('listeningSilenceDurationMax') ? opt.listeningSilenceDurationMax : this.opt.listeningSilenceDurationMax;
4266
+ this.listeningActiveThresholdLevel = opt?.hasOwnProperty('listeningActiveThresholdLevel') ? opt.listeningActiveThresholdLevel : this.opt.listeningActiveThresholdLevel;
4267
+ this.listeningActiveThresholdMs = opt?.hasOwnProperty('listeningActiveThresholdMs') ? opt.listeningActiveThresholdMs : this.opt.listeningActiveThresholdMs;
4268
+ this.listeningActiveDurationMax = opt?.hasOwnProperty('listeningActiveDurationMax') ? opt.listeningActiveDurationMax : this.opt.listeningActiveDurationMax;
4269
+
4270
+ this.listeningActive = false;
4271
+ this.listeningVolume = 0;
4272
+ this.listeningTimer = 0;
4273
+ this.listeningTimerTotal = 0;
4274
+ this.isListening = true;
4275
+ }
4276
+
4277
+ /**
4278
+ * Stop animation cycle.
4279
+ */
4280
+ stopListening() {
4281
+ this.isListening = false;
4282
+ }
4283
+
4284
+ /**
4285
+ * Play RPM/Mixamo animation clip.
4286
+ * @param {string|Object} url URL to animation file FBX
4287
+ * @param {progressfn} [onprogress=null] Callback for progress
4288
+ * @param {number} [dur=10] Duration in seconds, but at least once
4289
+ * @param {number} [ndx=0] Index of the clip
4290
+ * @param {number} [scale=0.01] Position scale factor
4291
+ */
4292
+ async playAnimation(url, onprogress=null, dur=10, ndx=0, scale=0.01) {
4293
+ if ( !this.armature ) return;
4294
+
4295
+ let item = this.animClips.find( x => x.url === url+'-'+ndx );
4296
+ if ( item ) {
4297
+
4298
+ // Reset pose update
4299
+ let anim = this.animQueue.find( x => x.template.name === 'pose' );
4300
+ if ( anim ) {
4301
+ anim.ts[0] = Infinity;
4302
+ }
4303
+
4304
+ // Set new pose
4305
+ Object.entries(item.pose.props).forEach( x => {
4306
+ this.poseBase.props[x[0]] = x[1].clone();
4307
+ this.poseTarget.props[x[0]] = x[1].clone();
4308
+ this.poseTarget.props[x[0]].t = 0;
4309
+ this.poseTarget.props[x[0]].d = 1000;
4310
+ });
4311
+
4312
+ // Create a new mixer
4313
+ if (this.mixer) {
4314
+ this.mixer.removeEventListener('finished', this._mixerHandler);
4315
+ this.mixer.stopAllAction();
4316
+ this.mixer.uncacheRoot(this.armature);
4317
+ this.mixer = null;
4318
+ this._mixerHandler = null;
4319
+ }
4320
+ this.mixer = new THREE.AnimationMixer(this.armature);
4321
+ this._mixerHandler = () => {
4322
+ this.stopAnimation();
4323
+ this.mixer?.removeEventListener('finished', this._mixerHandler);
4324
+ };
4325
+ this.mixer.addEventListener('finished', this._mixerHandler);
4326
+
4327
+ // Play action
4328
+ const repeat = Math.ceil(dur / item.clip.duration);
4329
+ const action = this.mixer.clipAction(item.clip);
4330
+ action.setLoop( THREE.LoopRepeat, repeat );
4331
+ action.clampWhenFinished = true;
4332
+ action.fadeIn(0.5).play();
4333
+
4334
+ } else {
4335
+
4336
+ // Load animation
4337
+ const loader = new FBXLoader();
4338
+
4339
+ let fbx = await loader.loadAsync( url, onprogress );
4340
+
4341
+ if ( fbx && fbx.animations && fbx.animations[ndx] ) {
4342
+ let anim = fbx.animations[ndx];
4343
+
4344
+ // Rename and scale Mixamo tracks, create a pose
4345
+ const props = {};
4346
+ anim.tracks.forEach( t => {
4347
+ t.name = t.name.replaceAll('mixamorig','');
4348
+ const ids = t.name.split('.');
4349
+ if ( ids[1] === 'position' ) {
4350
+ for(let i=0; i<t.values.length; i++ ) {
4351
+ t.values[i] = t.values[i] * scale;
4352
+ }
4353
+ props[t.name] = new THREE.Vector3(t.values[0],t.values[1],t.values[2]);
4354
+ } else if ( ids[1] === 'quaternion' ) {
4355
+ props[t.name] = new THREE.Quaternion(t.values[0],t.values[1],t.values[2],t.values[3]);
4356
+ } else if ( ids[1] === 'rotation' ) {
4357
+ props[ids[0]+".quaternion"] = new THREE.Quaternion().setFromEuler(new THREE.Euler(t.values[0],t.values[1],t.values[2],'XYZ')).normalize();
4358
+ }
4359
+
4360
+ });
4361
+
4362
+ // Add to clips
4363
+ const newPose = { props: props };
4364
+ if ( props['Hips.position'] ) {
4365
+ if ( props['Hips.position'].y < 0.5 ) {
4366
+ newPose.lying = true;
4367
+ } else {
4368
+ newPose.standing = true;
4369
+ }
4370
+ }
4371
+ this.animClips.push({
4372
+ url: url+'-'+ndx,
4373
+ clip: anim,
4374
+ pose: newPose
4375
+ });
4376
+
4377
+ // Play
4378
+ this.playAnimation(url, onprogress, dur, ndx, scale);
4379
+
4380
+ } else {
4381
+ const msg = 'Animation ' + url + ' (ndx=' + ndx + ') not found';
4382
+ console.error(msg);
4383
+ }
4384
+ }
4385
+ }
4386
+
4387
+ /**
4388
+ * Stop running animations.
4389
+ */
4390
+ stopAnimation() {
4391
+
4392
+ // Stop mixer
4393
+ if (this.mixer) {
4394
+ this.mixer.removeEventListener('finished', this._mixerHandler);
4395
+ this.mixer.stopAllAction();
4396
+ this.mixer.uncacheRoot(this.armature);
4397
+ this.mixer = null;
4398
+ this._mixerHandler = null;
4399
+ }
4400
+
4401
+ // Restart gesture
4402
+ if ( this.gesture ) {
4403
+ for( let [p,v] of Object.entries(this.gesture) ) {
4404
+ v.t = this.animClock;
4405
+ v.d = 1000;
4406
+ if ( this.poseTarget.props.hasOwnProperty(p) ) {
4407
+ this.poseTarget.props[p].copy(v);
4408
+ this.poseTarget.props[p].t = this.animClock;
4409
+ this.poseTarget.props[p].d = 1000;
4410
+ }
4411
+ }
4412
+ }
4413
+
4414
+ // Restart pose animation
4415
+ let anim = this.animQueue.find( x => x.template.name === 'pose' );
4416
+ if ( anim ) {
4417
+ anim.ts[0] = this.animClock;
4418
+ }
4419
+ this.setPoseFromTemplate( null );
4420
+
4421
+ }
4422
+
4423
+
4424
+ /**
4425
+ * Play RPM/Mixamo pose.
4426
+ * @param {string|Object} url Pose name | URL to FBX
4427
+ * @param {progressfn} [onprogress=null] Callback for progress
4428
+ * @param {number} [dur=5] Duration of the pose in seconds
4429
+ * @param {number} [ndx=0] Index of the clip
4430
+ * @param {number} [scale=0.01] Position scale factor
4431
+ */
4432
+ async playPose(url, onprogress=null, dur=5, ndx=0, scale=0.01) {
4433
+
4434
+ if ( !this.armature ) return;
4435
+
4436
+ // Check if we already have the pose template ready
4437
+ let pose = this.poseTemplates[url];
4438
+ if ( !pose ) {
4439
+ const item = this.animPoses.find( x => x.url === url+'-'+ndx );
4440
+ if ( item ) {
4441
+ pose = item.pose;
4442
+ }
4443
+ }
4444
+
4445
+ // If we have the template, use it, otherwise try to load it
4446
+ if ( pose ) {
4447
+
4448
+ this.poseName = url;
4449
+
4450
+ if (this.mixer) {
4451
+ this.mixer.removeEventListener('finished', this._mixerHandler);
4452
+ this.mixer.stopAllAction();
4453
+ this.mixer.uncacheRoot(this.armature);
4454
+ this.mixer = null;
4455
+ this._mixerHandler = null;
4456
+ }
4457
+ let anim = this.animQueue.find( x => x.template.name === 'pose' );
4458
+ if ( anim ) {
4459
+ anim.ts[0] = this.animClock + (dur * 1000) + 2000;
4460
+ }
4461
+ this.setPoseFromTemplate( pose );
4462
+
4463
+ } else {
4464
+
4465
+ // Load animation
4466
+ const loader = new FBXLoader();
4467
+
4468
+ let fbx = await loader.loadAsync( url, onprogress );
4469
+
4470
+ if ( fbx && fbx.animations && fbx.animations[ndx] ) {
4471
+ let anim = fbx.animations[ndx];
4472
+
4473
+ // Create a pose
4474
+ const props = {};
4475
+ anim.tracks.forEach( t => {
4476
+
4477
+ // Rename and scale Mixamo tracks
4478
+ t.name = t.name.replaceAll('mixamorig','');
4479
+ const ids = t.name.split('.');
4480
+ if ( ids[1] === 'position' ) {
4481
+ props[t.name] = new THREE.Vector3( t.values[0] * scale, t.values[1] * scale, t.values[2] * scale);
4482
+ } else if ( ids[1] === 'quaternion' ) {
4483
+ props[t.name] = new THREE.Quaternion( t.values[0], t.values[1], t.values[2], t.values[3] );
4484
+ } else if ( ids[1] === 'rotation' ) {
4485
+ props[ids[0]+".quaternion"] = new THREE.Quaternion().setFromEuler(new THREE.Euler( t.values[0], t.values[1], t.values[2],'XYZ' )).normalize();
4486
+ }
4487
+ });
4488
+
4489
+ // Add to pose
4490
+ const newPose = { props: props };
4491
+ if ( props['Hips.position'] ) {
4492
+ if ( props['Hips.position'].y < 0.5 ) {
4493
+ newPose.lying = true;
4494
+ } else {
4495
+ newPose.standing = true;
4496
+ }
4497
+ }
4498
+ this.animPoses.push({
4499
+ url: url+'-'+ndx,
4500
+ pose: newPose
4501
+ });
4502
+
4503
+ // Play
4504
+ this.playPose(url, onprogress, dur, ndx, scale);
4505
+
4506
+ } else {
4507
+ const msg = 'Pose ' + url + ' (ndx=' + ndx + ') not found';
4508
+ console.error(msg);
4509
+ }
4510
+ }
4511
+ }
4512
+
4513
+ /**
4514
+ * Stop the pose. (Functionality is the same as in stopAnimation.)
4515
+ */
4516
+ stopPose() {
4517
+ this.stopAnimation();
4518
+ }
4519
+
4520
+ /**
4521
+ * Play a gesture, which is either a hand gesture, an emoji animation or their
4522
+ * combination.
4523
+ * @param {string} name Gesture name
4524
+ * @param {number} [dur=3] Duration of the gesture in seconds
4525
+ * @param {boolean} [mirror=false] Mirror gesture
4526
+ * @param {number} [ms=1000] Transition time in milliseconds
4527
+ */
4528
+ playGesture(name, dur=3, mirror=false, ms=1000) {
4529
+
4530
+ if ( !this.armature ) return;
4531
+
4532
+ // Hand gesture, if any
4533
+ let g = this.gestureTemplates[name];
4534
+ if ( g ) {
4535
+
4536
+ // New gesture always overrides the existing one
4537
+ if ( this.gestureTimeout ) {
4538
+ clearTimeout( this.gestureTimeout );
4539
+ this.gestureTimeout = null;
4540
+ }
4541
+
4542
+ // Stop talking hands animation
4543
+ let ndx = this.animQueue.findIndex( y => y.template.name === "talkinghands" );
4544
+ if ( ndx !== -1 ) {
4545
+ this.animQueue[ndx].ts = this.animQueue[ndx].ts.map( x => 0 );
4546
+ }
4547
+
4548
+ // Set gesture
4549
+ this.gesture = this.propsToThreeObjects( g );
4550
+ if ( mirror ) {
4551
+ this.gesture = this.mirrorPose( this.gesture );
4552
+ }
4553
+ if ( name === "namaste" && this.avatar.body === 'M' ) {
4554
+ // Work-a-round for male model so that the hands meet
4555
+ this.gesture["RightArm.quaternion"].rotateTowards( new THREE.Quaternion(0,1,0,0), -0.25);
4556
+ this.gesture["LeftArm.quaternion"].rotateTowards( new THREE.Quaternion(0,1,0,0), -0.25);
4557
+ }
4558
+
4559
+ // Apply to target
4560
+ for( let [p,val] of Object.entries(this.gesture) ) {
4561
+ val.t = this.animClock;
4562
+ val.d = ms;
4563
+ if ( this.poseTarget.props.hasOwnProperty(p) ) {
4564
+ this.poseTarget.props[p].copy(val);
4565
+ this.poseTarget.props[p].t = this.animClock;
4566
+ this.poseTarget.props[p].d = ms;
4567
+ }
4568
+ }
4569
+
4570
+ // Timer
4571
+ if ( dur && Number.isFinite(dur) ) {
4572
+ this.gestureTimeout = setTimeout( this.stopGesture.bind(this,ms), 1000 * dur);
4573
+ }
4574
+ }
4575
+
4576
+ // Animated emoji, if any
4577
+ let em = this.animEmojis[name];
4578
+ if ( em ) {
4579
+
4580
+ // Follow link
4581
+ if ( em && em.link ) {
4582
+ em = this.animEmojis[em.link];
4583
+ }
4584
+
4585
+ if ( em ) {
4586
+ // Look at the camera for 500 ms
4587
+ this.lookAtCamera(500);
4588
+
4589
+ // Create animation and tag as gesture
4590
+ const anim = this.animFactory( em );
4591
+ anim.gesture = true;
4592
+
4593
+ // Rescale duration
4594
+ if ( dur && Number.isFinite(dur) ) {
4595
+ const first = anim.ts[0];
4596
+ const last = anim.ts[ anim.ts.length -1 ];
4597
+ const total = last - first;
4598
+ const excess = (dur * 1000) - total;
4599
+
4600
+ // If longer, increase longer parts; if shorter, scale everything
4601
+ if ( excess > 0 ) {
4602
+ const dt = [];
4603
+ for( let i=1; i<anim.ts.length; i++ ) dt.push( anim.ts[i] - anim.ts[i-1] );
4604
+ const rescale = em.template?.rescale || dt.map( x => x / total );
4605
+ const excess = dur * 1000 - total;
4606
+ anim.ts = anim.ts.map( (x,i,arr) => {
4607
+ return (i===0) ? first : (arr[i-1] + dt[i-1] + rescale[i-1] * excess);
4608
+ });
4609
+ } else {
4610
+ const scale = (dur * 1000) / total;
4611
+ anim.ts = anim.ts.map( x => first + scale * (x - first) );
4612
+ }
4613
+ }
4614
+
4615
+ this.animQueue.push( anim );
4616
+ }
4617
+ }
4618
+
4619
+ }
4620
+
4621
+ /**
4622
+ * Stop the gesture.
4623
+ * @param {number} [ms=1000] Transition time in milliseconds
4624
+ */
4625
+ stopGesture(ms=1000) {
4626
+
4627
+ // Stop gesture timer
4628
+ if ( this.gestureTimeout ) {
4629
+ clearTimeout( this.gestureTimeout );
4630
+ this.gestureTimeout = null;
4631
+ }
4632
+
4633
+ // Stop hand gesture, if any
4634
+ if ( this.gesture ) {
4635
+ const gs = Object.entries(this.gesture);
4636
+ this.gesture = null;
4637
+ for( const [p,val] of gs ) {
4638
+ if ( this.poseTarget.props.hasOwnProperty(p) ) {
4639
+ this.poseTarget.props[p].copy( this.getPoseTemplateProp(p) );
4640
+ this.poseTarget.props[p].t = this.animClock;
4641
+ this.poseTarget.props[p].d = ms;
4642
+ }
4643
+ }
4644
+ }
4645
+
4646
+ // Stop animated emoji gesture, if any
4647
+ let i = this.animQueue.findIndex( y => y.gesture );
4648
+ if ( i !== -1 ) {
4649
+ this.animQueue.splice(i, 1);
4650
+ }
4651
+
4652
+ }
4653
+
4654
+ /**
4655
+ * Cyclic Coordinate Descent (CCD) Inverse Kinematic (IK) algorithm.
4656
+ * Adapted from:
4657
+ * https://github.com/mrdoob/three.js/blob/master/examples/jsm/animation/CCDIKSolver.js
4658
+ * @param {Object} ik IK configuration object
4659
+ * @param {Vector3} [target=null] Target coordinate, if null return to template
4660
+ * @param {Boolean} [relative=false] If true, target is relative to root
4661
+ * @param {numeric} [d=null] If set, apply in d milliseconds
4662
+ */
4663
+ ikSolve(ik, target=null, relative=false, d=null) {
4664
+ const targetVec = new THREE.Vector3();
4665
+ const effectorPos = new THREE.Vector3();
4666
+ const effectorVec = new THREE.Vector3();
4667
+ const linkPos = new THREE.Vector3();
4668
+ const invLinkQ = new THREE.Quaternion();
4669
+ const linkScale = new THREE.Vector3();
4670
+ const axis = new THREE.Vector3();
4671
+ const vector = new THREE.Vector3();
4672
+
4673
+ // Reset IK setup positions and rotations
4674
+ const root = this.ikMesh.getObjectByName(ik.root);
4675
+ root.position.setFromMatrixPosition( this.armature.getObjectByName(ik.root).matrixWorld );
4676
+ root.quaternion.setFromRotationMatrix( this.armature.getObjectByName(ik.root).matrixWorld );
4677
+ if ( target && relative ) {
4678
+ target.applyQuaternion(this.armature.quaternion).add( root.position );
4679
+ }
4680
+ const effector = this.ikMesh.getObjectByName(ik.effector);
4681
+ const links = ik.links;
4682
+ links.forEach( x => {
4683
+ x.bone = this.ikMesh.getObjectByName(x.link);
4684
+ x.bone.quaternion.copy( this.getPoseTemplateProp(x.link+'.quaternion') );
4685
+ });
4686
+ root.updateMatrixWorld(true);
4687
+ const iterations = ik.iterations || 10;
4688
+
4689
+ // Iterate
4690
+ if ( target ) {
4691
+ for ( let i = 0; i < iterations; i ++ ) {
4692
+ let rotated = false;
4693
+ for ( let j = 0, jl = links.length; j < jl; j++ ) {
4694
+ const bone = links[j].bone;
4695
+ bone.matrixWorld.decompose( linkPos, invLinkQ, linkScale );
4696
+ invLinkQ.invert();
4697
+ effectorPos.setFromMatrixPosition( effector.matrixWorld );
4698
+ effectorVec.subVectors( effectorPos, linkPos );
4699
+ effectorVec.applyQuaternion( invLinkQ );
4700
+ effectorVec.normalize();
4701
+ targetVec.subVectors( target, linkPos );
4702
+ targetVec.applyQuaternion( invLinkQ );
4703
+ targetVec.normalize();
4704
+ let angle = targetVec.dot( effectorVec );
4705
+ if ( angle > 1.0 ) {
4706
+ angle = 1.0;
4707
+ } else if ( angle < - 1.0 ) {
4708
+ angle = - 1.0;
4709
+ }
4710
+ angle = Math.acos( angle );
4711
+ if ( angle < 1e-5 ) continue;
4712
+ if ( links[j].minAngle !== undefined && angle < links[j].minAngle ) {
4713
+ angle = links[j].minAngle;
4714
+ }
4715
+ if ( links[j].maxAngle !== undefined && angle > links[j].maxAngle ) {
4716
+ angle = links[j].maxAngle;
4717
+ }
4718
+ axis.crossVectors( effectorVec, targetVec );
4719
+ axis.normalize();
4720
+ q.setFromAxisAngle( axis, angle );
4721
+ bone.quaternion.multiply( q );
4722
+
4723
+ // Constraints
4724
+ bone.rotation.setFromVector3( vector.setFromEuler( bone.rotation ).clamp( new THREE.Vector3(
4725
+ links[j].minx !== undefined ? links[j].minx : -Infinity,
4726
+ links[j].miny !== undefined ? links[j].miny : -Infinity,
4727
+ links[j].minz !== undefined ? links[j].minz : -Infinity
4728
+ ), new THREE.Vector3(
4729
+ links[j].maxx !== undefined ? links[j].maxx : Infinity,
4730
+ links[j].maxy !== undefined ? links[j].maxy : Infinity,
4731
+ links[j].maxz !== undefined ? links[j].maxz : Infinity
4732
+ )) );
4733
+
4734
+ bone.updateMatrixWorld( true );
4735
+ rotated = true;
4736
+ }
4737
+ if ( !rotated ) break;
4738
+ }
4739
+ }
4740
+
4741
+ // Apply
4742
+ if ( d ) {
4743
+ links.forEach( x => {
4744
+ this.poseTarget.props[x.link+".quaternion"].copy( x.bone.quaternion );
4745
+ this.poseTarget.props[x.link+".quaternion"].t = this.animClock;
4746
+ this.poseTarget.props[x.link+".quaternion"].d = d;
4747
+ });
4748
+ }
4749
+ }
4750
+
4751
+ /**
4752
+ * Dispose the instance.
4753
+ */
4754
+ dispose() {
4755
+
4756
+ // Stop animation, clear speech queue, stop stream
4757
+ this.stop();
4758
+ this.stopSpeaking();
4759
+ this.streamStop();
4760
+ this.stopAnimation();
4761
+
4762
+ // Cancel animation frame to prevent potential memory leak
4763
+ if (this._raf !== null) {
4764
+ cancelAnimationFrame(this._raf);
4765
+ this._raf = null;
4766
+ }
4767
+
4768
+ // Stop & disconnect buffer sources
4769
+ ['audioSpeechSource', 'audioBackgroundSource'].forEach(key => {
4770
+ const node = this[key];
4771
+ if (node) {
4772
+ try { node.stop?.() } catch(error) {};
4773
+ node.disconnect();
4774
+ node.onended = null; // remove closure references
4775
+ }
4776
+ });
4777
+
4778
+ // Disconnect gain nodes & analyser
4779
+ ['audioBackgroundGainNode', 'audioSpeechGainNode',
4780
+ 'audioStreamGainNode', 'audioAnalyzerNode'].forEach(key => {
4781
+ const node = this[key];
4782
+ if (node) {
4783
+ node.disconnect();
4784
+ }
4785
+ });
4786
+
4787
+ // Dispose Three.JS objects
4788
+ if ( this.isAvatarOnly ) {
4789
+ if ( this.armature ) {
4790
+ if ( this.armature.parent ) {
4791
+ this.armature.parent.remove(this.armature);
4792
+ }
4793
+ this.clearThree(this.armature);
4794
+ }
4795
+ } else {
4796
+ this.clearThree(this.scene);
4797
+ this.resizeobserver.disconnect();
4798
+ this.resizeobserver = null;
4799
+
4800
+ if ( this.renderer ) {
4801
+ this.renderer.dispose();
4802
+ const gl = this.renderer.getContext();
4803
+ gl.getExtension('WEBGL_lose_context')?.loseContext();
4804
+ this.renderer.domElement?.remove();
4805
+ this.renderer.domElement = null;
4806
+ this.renderer = null;
4807
+ }
4808
+
4809
+ if ( this.controls ) {
4810
+ this.controls.dispose();
4811
+ this.controls = null;
4812
+ }
4813
+ }
4814
+
4815
+ this.clearThree( this.ikMesh );
4816
+ this.dynamicbones.dispose();
4817
+
4818
+ // DOM
4819
+ this.nodeAvatar = null;
4820
+
4821
+ }
4822
+
4823
+ }
4824
+
4825
+ export { TalkingHead };