@sage-rsc/talking-head-react 1.3.8 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1 -7
- package/dist/index.js +2391 -3094
- package/package.json +1 -1
- package/scripts/merge-talkinghead.js +108 -0
- package/scripts/update-talkinghead.sh +51 -0
- package/src/lib/talkinghead.mjs +1031 -1698
- package/src/lib/talkinghead.mjs.backup +6242 -0
- package/src/lib/talkinghead.mjs.new +5549 -0
- package/src/lib/talkinghead.mjs.upstream +4825 -0
|
@@ -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('&','&')
|
|
3296
|
+
.replaceAll('<','<')
|
|
3297
|
+
.replaceAll('>','>')
|
|
3298
|
+
.replaceAll('"','"')
|
|
3299
|
+
.replaceAll('\'',''')
|
|
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 };
|