1
|
import { TextWriter } from './text-writer.js';
|
2
|
import {VRSPACEUI} from './vrspace-ui.js';
|
3
|
|
4
|
/**
|
5
|
GLTF 3D Avatar.
|
6
|
Once GLTF file is loaded, skeleton is inspected for existing arms, legs and head that can be animated.
|
7
|
Animation groups are also inspected and optionally modified.
|
8
|
Optional fixes can be applied to an avatar, typically position of an avatar, or changing the animation.
|
9
|
*/
|
10
|
export class Avatar {
|
11
|
/**
|
12
|
@param scene
|
13
|
@param folder ServerFolder with the content
|
14
|
@param shadowGenerator optional to cast shadows
|
15
|
*/
|
16
|
constructor(scene, folder, shadowGenerator) {
|
17
|
// parameters
|
18
|
this.scene = scene;
|
19
|
/** ServerFolder with content path */
|
20
|
this.folder = folder;
|
21
|
/** File name, default scene.gltf */
|
22
|
this.file = folder.file?folder.file:"scene.gltf";
|
23
|
/** Optional ShadowGenerator */
|
24
|
this.shadowGenerator = shadowGenerator;
|
25
|
/** Mirror mode, default true. (Switch left/right side) */
|
26
|
this.mirror = true;
|
27
|
/** Animation frames per second, default 10 */
|
28
|
this.fps = 10;
|
29
|
/** Name of the avatar/user */
|
30
|
this.name = 'test';
|
31
|
/** Height of the user, default 1.8 */
|
32
|
this.userHeight = 1.8;
|
33
|
/** Height of the ground, default 0 */
|
34
|
this.groundHeight = 0;
|
35
|
/** Object containing fixes */
|
36
|
this.fixes = null;
|
37
|
/** Wheter to generate animations for arm movement, default true */
|
38
|
this.animateArms = true;
|
39
|
/** Return to rest after cloning, default true (otherwise keeps the pose)*/
|
40
|
this.returnToRest = true;
|
41
|
/** GLTF characters are facing the user when loaded, turn it around, default false*/
|
42
|
this.turnAround = false;
|
43
|
/** Object containing author, license, source, title */
|
44
|
this.info = null;
|
45
|
// state variables
|
46
|
/** Once the avatar is loaded an processed, body contains body parts, e.g. body.leftArm, body.rightLeg, body.neck */
|
47
|
this.body = {};
|
48
|
/** Contains the skeleton once the avatar is loaded and processed */
|
49
|
this.skeleton = null;
|
50
|
/** Parent mesh of the avatar, used for movement and attachment */
|
51
|
this.parentMesh = null;
|
52
|
/** Original root mesh of the avatar, used to scale the avatar */
|
53
|
this.rootMesh = null;
|
54
|
this.bonesTotal = 0;
|
55
|
this.bonesProcessed = [];
|
56
|
this.bonesDepth = 0;
|
57
|
this.animationTargets = [];
|
58
|
this.character = null;
|
59
|
this.activeAnimation = null;
|
60
|
this.writer = new TextWriter(this.scene);
|
61
|
this.writer.billboardMode = BABYLON.Mesh.BILLBOARDMODE_ALL;
|
62
|
/** fetch API cache control - use no-cache in development */
|
63
|
this.cache = 'default';
|
64
|
//this.cache = 'no-cache';
|
65
|
|
66
|
/** Debug output, default false */
|
67
|
this.debug = false;
|
68
|
this.debugViewer1;
|
69
|
this.debugViewer2;
|
70
|
}
|
71
|
|
72
|
createBody() {
|
73
|
this.bonesTotal = 0;
|
74
|
this.bonesProcessed = [];
|
75
|
this.bonesDepth = 0;
|
76
|
this.neckQuat = null;
|
77
|
this.neckQuatInv = null;
|
78
|
this.headQuat = null;
|
79
|
this.headQuatInv = null;
|
80
|
this.body = {
|
81
|
processed: false,
|
82
|
root: null,
|
83
|
hips: null, // aka pelvis
|
84
|
leftLeg: {
|
85
|
upper: null,
|
86
|
lower: null,
|
87
|
foot: [] // foot, toe, possibly more
|
88
|
},
|
89
|
rightLeg: {
|
90
|
upper: null,
|
91
|
lower: null,
|
92
|
foot: []
|
93
|
},
|
94
|
spine: [], // can have one or more segments
|
95
|
// aka clavicle
|
96
|
leftArm: {
|
97
|
side: 'left',
|
98
|
frontAxis: null,
|
99
|
sideAxis: null,
|
100
|
shoulder: null,
|
101
|
upper: null,
|
102
|
upperRot: null,
|
103
|
lower: null,
|
104
|
lowerRot: null,
|
105
|
hand: null,
|
106
|
handRot: null,
|
107
|
fingers: {
|
108
|
thumb: [],
|
109
|
index: [],
|
110
|
middle: [],
|
111
|
ring: [],
|
112
|
pinky: []
|
113
|
}
|
114
|
},
|
115
|
rightArm: {
|
116
|
side: 'right',
|
117
|
frontAxis: null,
|
118
|
sideAxis: null,
|
119
|
shoulder: null,
|
120
|
upper: null,
|
121
|
upperRot: null,
|
122
|
lower: null,
|
123
|
lowerRot: null,
|
124
|
hand: null,
|
125
|
handRot: null,
|
126
|
fingers: {
|
127
|
thumb: [],
|
128
|
index: [],
|
129
|
middle: [],
|
130
|
ring: [],
|
131
|
pinky: []
|
132
|
}
|
133
|
},
|
134
|
neck: {
|
135
|
neck: null,
|
136
|
head: null,
|
137
|
lefEye: null,
|
138
|
rightEye: null
|
139
|
}
|
140
|
};
|
141
|
};
|
142
|
|
143
|
log( anything ) {
|
144
|
if ( this.debug ) {
|
145
|
console.log( anything );
|
146
|
}
|
147
|
}
|
148
|
|
149
|
boneProcessed(bone) {
|
150
|
if ( this.bonesProcessed.includes(bone.name) ) {
|
151
|
this.log("Already processed bone "+bone.name);
|
152
|
} else {
|
153
|
this.bonesTotal++;
|
154
|
this.bonesProcessed.push(bone.name);
|
155
|
//this.log("Processed bone "+bone.name);
|
156
|
}
|
157
|
}
|
158
|
|
159
|
/** Dispose of everything */
|
160
|
dispose() {
|
161
|
if ( this.character ) {
|
162
|
VRSPACEUI.assetLoader.unloadAsset(this.getUrl(), this.instantiatedEntries);
|
163
|
delete this.instantiatedEntries;
|
164
|
this.character = null;
|
165
|
//delete this.character.avatar;
|
166
|
//this.character.dispose();
|
167
|
}
|
168
|
if ( this.debugViewer1 ) {
|
169
|
this.debugViewer1.dispose();
|
170
|
}
|
171
|
if ( this.debugViewer2 ) {
|
172
|
this.debugViewer2.dispose();
|
173
|
}
|
174
|
if ( this.nameTag ) {
|
175
|
this.nameTag.dispose();
|
176
|
}
|
177
|
if ( this.nameMesh ) {
|
178
|
this.nameMesh.dispose();
|
179
|
this.nameParent.dispose();
|
180
|
}
|
181
|
if (this.textParent) {
|
182
|
this.textParent.dispose();
|
183
|
this.textParent = null;
|
184
|
}
|
185
|
// TODO also dispose of materials and textures (asset container)
|
186
|
}
|
187
|
|
188
|
/**
|
189
|
Utility method, dispose of avatar and return this one.
|
190
|
@param avatar optional avatar to dispose of
|
191
|
*/
|
192
|
replace(avatar) {
|
193
|
if (avatar) {
|
194
|
avatar.dispose();
|
195
|
}
|
196
|
return this;
|
197
|
}
|
198
|
|
199
|
_processContainer( container, onSuccess ) {
|
200
|
this.character = container;
|
201
|
|
202
|
var meshes = container.meshes;
|
203
|
this.rootMesh = meshes[0];
|
204
|
this.animationTargets = [];
|
205
|
if ( this.turnAround ) {
|
206
|
// GLTF characters are facing the user when loaded, turn it around
|
207
|
this.rootMesh.rotationQuaternion = this.rootMesh.rotationQuaternion.multiply(BABYLON.Quaternion.RotationAxis(BABYLON.Axis.Y,Math.PI));
|
208
|
}
|
209
|
|
210
|
if (container.animationGroups && container.animationGroups.length > 0) {
|
211
|
container.animationGroups[0].stop();
|
212
|
}
|
213
|
|
214
|
this.animationTargets.sort((a, b) => a.localeCompare(b, undefined, {sensitivity: 'base'}));
|
215
|
|
216
|
var bbox = this.rootMesh.getHierarchyBoundingVectors();
|
217
|
this.log("Bounding box:");
|
218
|
this.log(bbox);
|
219
|
var scale = this.userHeight/(bbox.max.y-bbox.min.y);
|
220
|
this.log("Scaling: "+scale);
|
221
|
this.rootMesh.scaling = new BABYLON.Vector3(scale,scale,scale);
|
222
|
|
223
|
// Adds all elements to the scene
|
224
|
container.addAllToScene();
|
225
|
this.castShadows( this.shadowGenerator );
|
226
|
|
227
|
// try to place feet on the ground
|
228
|
// CHECKME is this really guaranteed to work in every time?
|
229
|
bbox = this.rootMesh.getHierarchyBoundingVectors();
|
230
|
this.groundLevel(-bbox.min.y);
|
231
|
// CHECKME we may want to store the value in case we want to apply it again
|
232
|
|
233
|
if ( container.skeletons && container.skeletons.length > 0 ) {
|
234
|
// CHECKME: should we process multiple skeletons?
|
235
|
this.skeleton = container.skeletons[0];
|
236
|
|
237
|
this.createBody();
|
238
|
//this.log("bones: "+bonesTotal+" "+bonesProcessed);
|
239
|
|
240
|
this.skeleton.computeAbsoluteTransforms();
|
241
|
// different ways to enforce calculation:
|
242
|
//this.skeleton.computeAbsoluteTransforms(true);
|
243
|
//this.rootMesh.computeWorldMatrix(true);
|
244
|
//this.scene.render();
|
245
|
this.skeleton.name = this.folder.name;
|
246
|
|
247
|
this.processBones(this.skeleton.bones);
|
248
|
this.log( "Head position: "+this.headPos());
|
249
|
this.initialHeadPos = this.headPos();
|
250
|
this.resize();
|
251
|
|
252
|
//this.log(this.body);
|
253
|
this.bonesProcessed.sort((a, b) => a.localeCompare(b, undefined, {sensitivity: 'base'}));
|
254
|
|
255
|
this.calcLength(this.body.leftArm);
|
256
|
this.calcLength(this.body.rightArm);
|
257
|
this.calcLength(this.body.leftLeg);
|
258
|
this.calcLength(this.body.rightLeg);
|
259
|
this.guessArmsRotations();
|
260
|
this.guessLegsRotations();
|
261
|
|
262
|
this.body.processed = true;
|
263
|
|
264
|
if ( this.debugViewier1 || this.debugViewer2 ) {
|
265
|
this.scene.registerBeforeRender(() => {
|
266
|
if (this.debugViewer1) {
|
267
|
this.debugViewer1.update();
|
268
|
}
|
269
|
if (this.debugViewer2) {
|
270
|
this.debugViewer2.update();
|
271
|
}
|
272
|
});
|
273
|
}
|
274
|
} else {
|
275
|
console.log("NOT an avatar - no skeletons");
|
276
|
}
|
277
|
|
278
|
//this.postProcess();
|
279
|
|
280
|
this.parentMesh = container.createRootMesh();
|
281
|
this.parentMesh.rotationQuaternion = new BABYLON.Quaternion();
|
282
|
container.avatar = this;
|
283
|
|
284
|
console.log("Avatar loaded: "+this.name);
|
285
|
if ( onSuccess ) {
|
286
|
onSuccess(this);
|
287
|
}
|
288
|
}
|
289
|
|
290
|
/**
|
291
|
Apply fixes after loading/instantiation
|
292
|
*/
|
293
|
postProcess() {
|
294
|
if ( this.fixes ) {
|
295
|
if ( typeof this.fixes.standing !== 'undefined' ) {
|
296
|
// CHECKME not used since proper bounding box calculation
|
297
|
// might be required in some special cases
|
298
|
this.log( "Applying fixes for: "+this.folder.name+" standing: "+this.fixes.standing);
|
299
|
this.groundLevel(this.fixes.standing);
|
300
|
}
|
301
|
this.disableNodes();
|
302
|
if ( typeof this.fixes.autoPlay !== 'undefined' ) {
|
303
|
// start playing the animation
|
304
|
this.startAnimation(this.fixes.autoPlay);
|
305
|
}
|
306
|
}
|
307
|
|
308
|
}
|
309
|
/**
|
310
|
Load fixes from json file in the same folder, with the same name, and suffix .fixes.
|
311
|
Called from load().
|
312
|
*/
|
313
|
async loadFixes() {
|
314
|
this.log('Loading fixes from '+this.folder.relatedUrl());
|
315
|
if ( this.folder.related ) {
|
316
|
return fetch(this.folder.relatedUrl(), {cache: this.cache})
|
317
|
.then(response => {
|
318
|
if ( response.ok ) {
|
319
|
response.json().then(json => {
|
320
|
this.fixes = json;
|
321
|
this.log( "Loaded fixes: " );
|
322
|
this.log( json );
|
323
|
});
|
324
|
} else {
|
325
|
console.log('Error loading fixes: ' + response.status);
|
326
|
}
|
327
|
});
|
328
|
}
|
329
|
}
|
330
|
|
331
|
/**
|
332
|
Disable nodes marked in fixes file
|
333
|
*/
|
334
|
disableNodes() {
|
335
|
if ( typeof this.fixes.nodesDisabled !== 'undefined' ) {
|
336
|
this.enableNodes( this.fixes.nodesDisabled, false );
|
337
|
}
|
338
|
}
|
339
|
|
340
|
/**
|
341
|
Enable/disable given nodes
|
342
|
@param nodeIds array of node identifiers
|
343
|
@param enable true/false
|
344
|
*/
|
345
|
enableNodes( nodeIds, enable ) {
|
346
|
this.character.getNodes().forEach( node => {
|
347
|
if ( nodeIds.includes(node.id)) {
|
348
|
this.log("Node "+node.id+" enabled: "+enable);
|
349
|
node.setEnabled(enable);
|
350
|
}
|
351
|
});
|
352
|
}
|
353
|
|
354
|
/**
|
355
|
Slice an animation group
|
356
|
@param group AnimationGroup to slice
|
357
|
@param start starting key
|
358
|
@param end ending key
|
359
|
@returns new AnimationGroup containing slice of original animations
|
360
|
*/
|
361
|
sliceGroup( group, start, end ) {
|
362
|
var newGroup = new BABYLON.AnimationGroup(group.name+":"+start+"-"+end);
|
363
|
for ( var i = 0; i < group.targetedAnimations.length; i++ ) {
|
364
|
var slice = this.sliceAnimation( group.targetedAnimations[i].animation, start, end );
|
365
|
if ( slice.getKeys().length > 0 ) {
|
366
|
newGroup.addTargetedAnimation( slice, group.targetedAnimations[i].target );
|
367
|
}
|
368
|
}
|
369
|
return newGroup;
|
370
|
}
|
371
|
|
372
|
/**
|
373
|
Slice an animation
|
374
|
@param animation Animation to slice
|
375
|
@param start starting key
|
376
|
@param end ending key
|
377
|
@returns new Animation containing slice of original animation
|
378
|
*/
|
379
|
sliceAnimation(animation, start, end) {
|
380
|
var keys = animation.getKeys();
|
381
|
var slice = [];
|
382
|
for ( var i = 0; i < keys.length; i++ ) {
|
383
|
var key = keys[i];
|
384
|
if ( key.frame >= start ) {
|
385
|
if ( key.frame <= end ) {
|
386
|
slice.push(key);
|
387
|
} else {
|
388
|
break;
|
389
|
}
|
390
|
}
|
391
|
}
|
392
|
var ret = new BABYLON.Animation(animation.name, animation.targetProperty, animation.framePerSecond, animation.dataType, animation.enableBlending);
|
393
|
ret.loopMode = animation.loopMode;
|
394
|
ret.setKeys( slice );
|
395
|
return ret;
|
396
|
}
|
397
|
|
398
|
/**
|
399
|
Returns all animation groups of this avatar.
|
400
|
Applies fixes first, if any.
|
401
|
*/
|
402
|
getAnimationGroups(animationGroups = this.character.animationGroups) {
|
403
|
if (!this.animationGroups) {
|
404
|
var loopAnimations = true;
|
405
|
if ( this.fixes && typeof this.fixes.loopAnimations !== 'undefined' ) {
|
406
|
loopAnimations = this.fixes.loopAnimations;
|
407
|
}
|
408
|
if ( this.fixes && this.fixes.animationGroups ) {
|
409
|
this.animationGroups = [];
|
410
|
// animation groups overriden; process animation groups and generate new ones
|
411
|
for ( var j = 0; j < this.fixes.animationGroups.length; j++ ) {
|
412
|
var override = this.fixes.animationGroups[j];
|
413
|
// find source group
|
414
|
for ( var i = 0; i < animationGroups.length; i++ ) {
|
415
|
var group = animationGroups[i];
|
416
|
if ( group.name == override.source ) {
|
417
|
var newGroup = group;
|
418
|
if ( override.start || override.end ) {
|
419
|
// now slice it and generate new group
|
420
|
newGroup = this.sliceGroup( group, override.start, override.end );
|
421
|
}
|
422
|
if ( override.name ) {
|
423
|
newGroup.name = override.name;
|
424
|
}
|
425
|
if ( typeof override.loop !== 'undefined' ) {
|
426
|
newGroup.loopAnimation = override.loop;
|
427
|
} else {
|
428
|
newGroup.loopAnimation = loopAnimations;
|
429
|
}
|
430
|
this.animationGroups.push( newGroup );
|
431
|
break;
|
432
|
}
|
433
|
}
|
434
|
}
|
435
|
} else {
|
436
|
this.animationGroups = animationGroups;
|
437
|
for ( var i=0; i<this.animationGroups.length; i++ ) {
|
438
|
this.animationGroups[i].loopAnimation = loopAnimations;
|
439
|
}
|
440
|
}
|
441
|
}
|
442
|
return this.animationGroups;
|
443
|
}
|
444
|
|
445
|
/** Returns file name of this avatar, consisting of folder name and scene file name */
|
446
|
getUrl() {
|
447
|
return this.folder.url()+"/"+this.file;
|
448
|
}
|
449
|
|
450
|
/**
|
451
|
Loads the avatar.
|
452
|
@param success callback to execute on success
|
453
|
@param failure executed if loading fails
|
454
|
*/
|
455
|
load(success, failure) {
|
456
|
this.loadFixes().then( () => {
|
457
|
this.log("loading from "+this.folder.url());
|
458
|
var plugin = VRSPACEUI.assetLoader.loadAsset(
|
459
|
this.getUrl(),
|
460
|
// onSuccess:
|
461
|
(loadedUrl, container, info, instantiatedEntries ) => {
|
462
|
this.info = info
|
463
|
// https://doc.babylonjs.com/typedoc/classes/babylon.assetcontainer
|
464
|
// https://doc.babylonjs.com/typedoc/classes/babylon.instantiatedentries
|
465
|
if ( instantiatedEntries ) {
|
466
|
console.log("CHECKME: avatar "+this.name+" already loaded", container.avatar);
|
467
|
// copy body bones from processed avatar
|
468
|
this.character = container;
|
469
|
this.neckQuat = container.avatar.neckQuat;
|
470
|
this.neckQuatInv = container.avatar.neckQuatInv;
|
471
|
this.headQuat = container.avatar.headQuat;
|
472
|
this.headQuatInv = container.avatar.headQuatInv;
|
473
|
this.body = container.avatar.body;
|
474
|
// use skeleton and animationGroups from the instance
|
475
|
this.parentMesh = instantiatedEntries.rootNodes[0];
|
476
|
this.rootMesh = this.parentMesh.getChildren()[0];
|
477
|
if ( this.parentMesh.getChildren().length > 1 ) {
|
478
|
// clean up any existing text cloned along with container
|
479
|
console.log("Disposing of text ", this.parentMesh.getChildren()[1])
|
480
|
this.parentMesh.getChildren()[1].dispose();
|
481
|
}
|
482
|
this.getAnimationGroups(instantiatedEntries.animationGroups);
|
483
|
this.skeleton = instantiatedEntries.skeletons[0];
|
484
|
if ( this.returnToRest ) {
|
485
|
this.standUp();
|
486
|
this.skeleton.returnToRest();
|
487
|
}
|
488
|
this.parentMesh.rotationQuaternion = new BABYLON.Quaternion();
|
489
|
this.instantiatedEntries = instantiatedEntries;
|
490
|
if ( success ) {
|
491
|
success(this);
|
492
|
}
|
493
|
} else {
|
494
|
container.addAllToScene();
|
495
|
this._processContainer(container,success)
|
496
|
}
|
497
|
this.postProcess();
|
498
|
},
|
499
|
(exception)=>{
|
500
|
if ( failure ) {
|
501
|
failure(exception);
|
502
|
} else {
|
503
|
console.log(exception);
|
504
|
}
|
505
|
}
|
506
|
);
|
507
|
});
|
508
|
}
|
509
|
|
510
|
/** Returns position of the the head 'bone' */
|
511
|
headPos() {
|
512
|
var head = this.skeleton.bones[this.body.head];
|
513
|
//head.computeAbsoluteTransforms();
|
514
|
//head.getTransformNode().computeWorldMatrix(true);
|
515
|
//this.scene.render(); // FIXME workaround
|
516
|
console.log("Head at "+head.getAbsolutePosition()+" tran "+head.getTransformNode().getAbsolutePosition()+" root "+this.rootMesh.getAbsolutePosition(), head);
|
517
|
//var headPos = head.getAbsolutePosition().scale(this.rootMesh.scaling.x).add(this.rootMesh.position);
|
518
|
//var headPos = head.getTransformNode().getAbsolutePosition().subtract(this.rootMesh.getAbsolutePosition());
|
519
|
var headPos = head.getTransformNode().getAbsolutePosition();
|
520
|
return headPos;
|
521
|
}
|
522
|
|
523
|
/** Returns current height - distance head to feet */
|
524
|
height() {
|
525
|
return this.headPos().y - this.rootMesh.getAbsolutePosition().y;
|
526
|
}
|
527
|
|
528
|
/**
|
529
|
Returns absolute value of vector, i.e. Math.abs() of every value
|
530
|
@param vec Vector3 to get absolute
|
531
|
*/
|
532
|
absVector(vec) {
|
533
|
var ret = new BABYLON.Vector3();
|
534
|
ret.x = Math.abs(vec.x);
|
535
|
ret.y = Math.abs(vec.y);
|
536
|
ret.z = Math.abs(vec.z);
|
537
|
return ret;
|
538
|
}
|
539
|
|
540
|
/**
|
541
|
Returns rounded value of vector, i.e. Math.round() of every value
|
542
|
@param vec Vector3 to round
|
543
|
*/
|
544
|
roundVector(vec) {
|
545
|
vec.x = Math.round(vec.x);
|
546
|
vec.y = Math.round(vec.y);
|
547
|
vec.z = Math.round(vec.z);
|
548
|
}
|
549
|
|
550
|
/**
|
551
|
Look at given target. Head position is calculated without any bone limits.
|
552
|
@param t target Vector3
|
553
|
*/
|
554
|
lookAt( t ) {
|
555
|
var head = this.skeleton.bones[this.body.head];
|
556
|
|
557
|
// calc target pos in coordinate system of head
|
558
|
var totalPos = this.parentMesh.position.add(this.rootMesh.position);
|
559
|
var totalRot = this.rootMesh.rotationQuaternion.multiply(this.parentMesh.rotationQuaternion);
|
560
|
var target = new BABYLON.Vector3( t.x, t.y, t.z ).subtract(totalPos);
|
561
|
|
562
|
target.rotateByQuaternionToRef(BABYLON.Quaternion.Inverse(totalRot),target);
|
563
|
|
564
|
// CHECKME: exact calculus?
|
565
|
//var targetVector = target.subtract(this.headPos()).add(totalPos);
|
566
|
var targetVector = target.subtract(this.headPos());
|
567
|
if ( this.headAxisFix == -1 ) {
|
568
|
// FIX: neck and head opposite orientation
|
569
|
// businessman, robot, adventurer, unreal male
|
570
|
targetVector.y = -targetVector.y;
|
571
|
}
|
572
|
targetVector.rotateByQuaternionToRef(this.headQuatInv,targetVector);
|
573
|
// this results in weird head positions, more natural-looking fix applied after
|
574
|
//targetVector.rotateByQuaternionToRef(this.headQuat.multiply(this.neckQuatInv),targetVector);
|
575
|
|
576
|
var rotationMatrix = new BABYLON.Matrix();
|
577
|
|
578
|
BABYLON.Matrix.RotationAlignToRef(this.headTarget, targetVector.normalizeToNew(), rotationMatrix);
|
579
|
var quat = BABYLON.Quaternion.FromRotationMatrix(rotationMatrix);
|
580
|
|
581
|
if ( this.headAxisFix != 1 ) {
|
582
|
// FIX: neck and head opposite or under angle
|
583
|
// boris, businessman, robot, adventurer, unreal male
|
584
|
var fix = this.headQuat.multiply(this.neckQuatInv);
|
585
|
quat = quat.multiply(fix);
|
586
|
}
|
587
|
|
588
|
head.getTransformNode().rotationQuaternion = quat;
|
589
|
}
|
590
|
|
591
|
thirdAxis( limb ) {
|
592
|
var ret = new BABYLON.Vector3(1,1,1);
|
593
|
return ret.subtract(limb.frontAxis.axis).subtract(limb.sideAxis.axis);
|
594
|
}
|
595
|
|
596
|
/** Debugging helper, draws a vector between given points */
|
597
|
drawVector(from, to) {
|
598
|
BABYLON.MeshBuilder.CreateLines("vector-"+from+"-"+to, {points:[from,to]}, this.scene);
|
599
|
}
|
600
|
|
601
|
/**
|
602
|
Move given arm towards given target. Uses simplified 2-joint IK.
|
603
|
@param arm arm to move
|
604
|
@param t target position
|
605
|
*/
|
606
|
reachFor( arm, t ) {
|
607
|
|
608
|
var upperArm = this.skeleton.bones[arm.upper];
|
609
|
var lowerArm = this.skeleton.bones[arm.lower];
|
610
|
|
611
|
var scaling = this.rootMesh.scaling.x;
|
612
|
|
613
|
var totalPos = this.parentMesh.position.add(this.rootMesh.position);
|
614
|
var totalRot = this.parentMesh.rotationQuaternion.multiply(this.rootMesh.rotationQuaternion);
|
615
|
// current values
|
616
|
var armPos = upperArm.getAbsolutePosition().scale(scaling).subtract(totalPos);
|
617
|
var elbowPos = lowerArm.getAbsolutePosition().scale(scaling).subtract(totalPos);
|
618
|
|
619
|
var rootQuatInv = BABYLON.Quaternion.Inverse(totalRot);
|
620
|
|
621
|
// set or get initial values
|
622
|
if ( arm.upperQuat ) {
|
623
|
var upperQuat = arm.upperQuat;
|
624
|
var armVector = arm.armVector;
|
625
|
var worldQuat = arm.worldQuat;
|
626
|
var worldQuatInv = arm.worldQuatInv;
|
627
|
} else {
|
628
|
var worldQuat = BABYLON.Quaternion.FromRotationMatrix(upperArm.getWorldMatrix().getRotationMatrix());
|
629
|
arm.worldQuat = worldQuat;
|
630
|
this.log("Arm angles: "+worldQuat.toEulerAngles());
|
631
|
var worldQuatInv = BABYLON.Quaternion.Inverse(worldQuat);
|
632
|
arm.worldQuatInv = worldQuatInv;
|
633
|
var upperQuat = upperArm.getRotationQuaternion();
|
634
|
arm.upperQuat = upperQuat;
|
635
|
var armVector = elbowPos.subtract(armPos);
|
636
|
armVector.rotateByQuaternionToRef(worldQuatInv,armVector);
|
637
|
arm.armVector = armVector;
|
638
|
}
|
639
|
|
640
|
// calc target pos in coordinate system of character
|
641
|
var target = new BABYLON.Vector3(t.x, t.y, t.z).subtract(totalPos);
|
642
|
// CHECKME: probable bug, possibly related to worldQuat
|
643
|
target.rotateByQuaternionToRef(rootQuatInv,target);
|
644
|
|
645
|
// calc target vectors in local coordinate system of the arm
|
646
|
var targetVector = target.subtract(armPos).subtract(totalPos);
|
647
|
targetVector.rotateByQuaternionToRef(worldQuatInv,targetVector);
|
648
|
|
649
|
if ( arm.pointerQuat ) {
|
650
|
|
651
|
// vector pointing down in local space:
|
652
|
var downVector = new BABYLON.Vector3(0,-1,0);
|
653
|
var downRotation = new BABYLON.Matrix();
|
654
|
BABYLON.Matrix.RotationAlignToRef(armVector.normalizeToNew(), downVector.normalizeToNew(), downRotation);
|
655
|
var downQuat = BABYLON.Quaternion.FromRotationMatrix(downRotation)
|
656
|
// (near) parallel vectors still causing trouble
|
657
|
if ( isNaN(downQuat.x) || isNaN(!downQuat.y) || isNaN(!downQuat.z) || isNaN(!downQuat.y) ) {
|
658
|
this.log("arm vector: "+armVector+"down vector: "+downVector+" quat: "+downQuat+" rot: ");
|
659
|
this.log(downRotation);
|
660
|
// TODO: front axis, sign
|
661
|
downQuat = BABYLON.Quaternion.FromEulerAngles(0,0,Math.PI);
|
662
|
}
|
663
|
armVector.rotateByQuaternionToRef(downQuat,downVector);
|
664
|
//this.drawVector(armPos, armPos.add(downVector));
|
665
|
|
666
|
// pointer vector in mesh space:
|
667
|
var pointerQuat = arm.pointerQuat.multiply(rootQuatInv);
|
668
|
if ( this.mirror ) {
|
669
|
// heuristics 1, mirrored arm rotation, works well below shoulder
|
670
|
pointerQuat.y = - pointerQuat.y;
|
671
|
// heuristics 2, never point backwards
|
672
|
//pointerQuat.z = - pointerQuat.z;
|
673
|
if ( pointerQuat.z < 0 ) {
|
674
|
pointerQuat.z = 0;
|
675
|
}
|
676
|
} else {
|
677
|
// funny though this seems to just work
|
678
|
}
|
679
|
|
680
|
var pointerVector = new BABYLON.Vector3();
|
681
|
downVector.rotateByQuaternionToRef(pointerQuat,pointerVector);
|
682
|
//this.drawVector(armPos, armPos.add(pointerVector));
|
683
|
// converted to local arm space:
|
684
|
var sideVector = new BABYLON.Vector3();
|
685
|
pointerVector.rotateByQuaternionToRef(worldQuatInv,sideVector);
|
686
|
|
687
|
// rotation from current to side
|
688
|
var sideRotation = new BABYLON.Matrix();
|
689
|
BABYLON.Matrix.RotationAlignToRef(armVector.normalizeToNew(), sideVector.normalizeToNew(), sideRotation);
|
690
|
// rotation from side to target
|
691
|
var targetRotation = new BABYLON.Matrix();
|
692
|
BABYLON.Matrix.RotationAlignToRef(sideVector.normalizeToNew(), targetVector.normalizeToNew(), targetRotation);
|
693
|
var finalRotation = sideRotation.multiply(targetRotation);
|
694
|
} else {
|
695
|
// just point arm to target
|
696
|
var finalRotation = new BABYLON.Matrix();
|
697
|
BABYLON.Matrix.RotationAlignToRef(armVector.normalizeToNew(), targetVector.normalizeToNew(), finalRotation);
|
698
|
}
|
699
|
|
700
|
var quat = BABYLON.Quaternion.FromRotationMatrix(finalRotation);
|
701
|
|
702
|
arm.upperRot = upperQuat.multiply(quat);
|
703
|
|
704
|
// then bend arm
|
705
|
var length = targetVector.length();
|
706
|
var bent = this.bendArm(arm, length);
|
707
|
|
708
|
this.renderArmRotation(arm);
|
709
|
return quat;
|
710
|
}
|
711
|
|
712
|
// move an arms, optionally creates/updates arm animation
|
713
|
renderArmRotation( arm ) {
|
714
|
var upperArm = this.skeleton.bones[arm.upper];
|
715
|
var lowerArm = this.skeleton.bones[arm.lower];
|
716
|
if ( ! this.animateArms ) {
|
717
|
upperArm.getTransformNode().rotationQuaternion = arm.upperRot;
|
718
|
lowerArm.getTransformNode().rotationQuaternion = arm.lowerRot;
|
719
|
return;
|
720
|
}
|
721
|
if ( !arm.animation ) {
|
722
|
var armName = this.folder.name+'-'+arm.side;
|
723
|
var group = new BABYLON.AnimationGroup(armName+'ArmAnimation');
|
724
|
|
725
|
var upper = this._createArmAnimation(armName+"-upper");
|
726
|
var lower = this._createArmAnimation(armName+"-lower");
|
727
|
|
728
|
group.addTargetedAnimation(upper, this.skeleton.bones[arm.upper].getTransformNode());
|
729
|
group.addTargetedAnimation(lower, this.skeleton.bones[arm.lower].getTransformNode());
|
730
|
arm.animation = group;
|
731
|
}
|
732
|
this._updateArmAnimation(upperArm, arm.animation.targetedAnimations[0], arm.upperRot);
|
733
|
this._updateArmAnimation(lowerArm, arm.animation.targetedAnimations[1], arm.lowerRot);
|
734
|
if ( arm.animation.isPlaying ) {
|
735
|
arm.animation.stop();
|
736
|
}
|
737
|
arm.animation.play(false);
|
738
|
}
|
739
|
|
740
|
_createArmAnimation(name) {
|
741
|
var anim = new BABYLON.Animation(name, 'rotationQuaternion', this.fps, BABYLON.Animation.ANIMATIONTYPE_QUATERNION, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
|
742
|
var keys = [];
|
743
|
keys.push({frame:0, value: 0});
|
744
|
keys.push({frame:1, value: 0});
|
745
|
anim.setKeys(keys);
|
746
|
return anim;
|
747
|
}
|
748
|
_updateArmAnimation(arm, anim, dest) {
|
749
|
anim.animation.getKeys()[0].value = arm.getTransformNode().rotationQuaternion;
|
750
|
anim.animation.getKeys()[1].value = dest;
|
751
|
}
|
752
|
|
753
|
/**
|
754
|
Bend/stretch arm to a length
|
755
|
@param arm
|
756
|
@param length
|
757
|
*/
|
758
|
bendArm( arm, length ) {
|
759
|
var ret = true;
|
760
|
var upperArm = this.skeleton.bones[arm.upper];
|
761
|
var lowerArm = this.skeleton.bones[arm.lower];
|
762
|
var scaling = this.rootMesh.scaling.x;
|
763
|
|
764
|
if ( length > arm.lowerLength + arm.upperLength ) {
|
765
|
length = arm.lowerLength + arm.upperLength
|
766
|
ret = false;
|
767
|
}
|
768
|
|
769
|
// simplified math by using same length for both bones
|
770
|
// it's right angle, hypotenuse is bone
|
771
|
// length/2 is sinus of half of elbow angle
|
772
|
var boneLength = (arm.lowerLength + arm.upperLength)/2;
|
773
|
var innerAngle = Math.asin(length/2/boneLength);
|
774
|
//this.log("Bone length: "+boneLength+" distance to target "+length);
|
775
|
var shoulderAngle = Math.PI/2-innerAngle;
|
776
|
var elbowAngle = shoulderAngle*2;
|
777
|
|
778
|
var fix = BABYLON.Quaternion.RotationAxis(arm.frontAxis.axis,-shoulderAngle*arm.frontAxis.sign);
|
779
|
arm.upperRot = arm.upperRot.multiply(fix);
|
780
|
|
781
|
arm.lowerRot = BABYLON.Quaternion.RotationAxis(arm.frontAxis.axis,elbowAngle*arm.frontAxis.sign);
|
782
|
//this.log("Angle shoulder: "+shoulderAngle+" elbow: "+elbowAngle+" length: "+length);
|
783
|
return ret;
|
784
|
}
|
785
|
|
786
|
legLength() {
|
787
|
return (this.body.leftLeg.upperLength + this.body.leftLeg.lowerLength + this.body.rightLeg.upperLength + this.body.rightLeg.lowerLength)/2;
|
788
|
}
|
789
|
|
790
|
/**
|
791
|
Set avatar position.
|
792
|
@param pos postion
|
793
|
*/
|
794
|
setPosition( pos ) {
|
795
|
this.parentMesh.position.x = pos.x;
|
796
|
//this.groundLevel( pos.y ); // CHECKME
|
797
|
this.parentMesh.position.y = pos.y;
|
798
|
this.parentMesh.position.z = pos.z;
|
799
|
}
|
800
|
|
801
|
/**
|
802
|
Set avatar rotation
|
803
|
@param quat Quaternion
|
804
|
*/
|
805
|
setRotation( quat ) {
|
806
|
// FIXME this should rotate parentMesh instead
|
807
|
// but GLTF characters are facing the user when loaded
|
808
|
this.parentMesh.rotationQuaternion = quat;
|
809
|
}
|
810
|
|
811
|
/**
|
812
|
Sets the ground level
|
813
|
@param y height of the ground at current position
|
814
|
*/
|
815
|
groundLevel( y ) {
|
816
|
this.groundHeight = y;
|
817
|
this.rootMesh.position.y = this.rootMesh.position.y + y;
|
818
|
}
|
819
|
|
820
|
/**
|
821
|
Track user height: character may crouch or raise, or jump,
|
822
|
depending on heights of avatar and user.
|
823
|
@param height current user height
|
824
|
*/
|
825
|
trackHeight(height) {
|
826
|
if ( this.maxUserHeight && height != this.prevUserHeight ) {
|
827
|
var delta = height-this.prevUserHeight;
|
828
|
//this.trackDelay = 1000/this.fps;
|
829
|
//var speed = delta/this.trackDelay*1000; // speed in m/s
|
830
|
var speed = delta*this.fps;
|
831
|
if ( this.jumping ) {
|
832
|
var delay = Date.now() - this.jumping;
|
833
|
if ( height <= this.maxUserHeight && delay > 300 ) {
|
834
|
this.standUp();
|
835
|
this.jumping = null;
|
836
|
this.log("jump stopped")
|
837
|
} else if ( delay > 500 ) {
|
838
|
this.log("jump stopped - timeout")
|
839
|
this.standUp();
|
840
|
this.jumping = null;
|
841
|
} else {
|
842
|
this.jump(height - this.maxUserHeight);
|
843
|
}
|
844
|
} else if ( height > this.maxUserHeight && Math.abs(speed) > 1 ) {
|
845
|
// CHECKME speed is not really important here
|
846
|
this.jump(height - this.maxUserHeight);
|
847
|
this.jumping = Date.now();
|
848
|
this.log("jump starting")
|
849
|
} else {
|
850
|
// ignoring anything less than 1mm
|
851
|
if ( delta > 0.001 ) {
|
852
|
this.rise(delta);
|
853
|
} else if ( delta < -0.001 ) {
|
854
|
this.crouch(-delta);
|
855
|
}
|
856
|
}
|
857
|
|
858
|
} else {
|
859
|
this.maxUserHeight = height;
|
860
|
}
|
861
|
this.prevUserHeight = height;
|
862
|
}
|
863
|
|
864
|
/**
|
865
|
Moves the avatar to given height above the ground
|
866
|
@param height jump how high
|
867
|
*/
|
868
|
jump( height ) {
|
869
|
this.rootMesh.position.y = this.groundHeight + height;
|
870
|
this.changed();
|
871
|
}
|
872
|
|
873
|
/**
|
874
|
Stand up straight, at the ground, legs fully stretched
|
875
|
*/
|
876
|
standUp() {
|
877
|
this.jump(0);
|
878
|
this.bendLeg( this.body.leftLeg, 10 );
|
879
|
this.bendLeg( this.body.rightLeg, 10 );
|
880
|
}
|
881
|
|
882
|
/**
|
883
|
Rise a bit
|
884
|
@param height rise how much
|
885
|
*/
|
886
|
rise( height ) {
|
887
|
if ( height < 0.001 ) {
|
888
|
// ignoring anything less than 1mm
|
889
|
return;
|
890
|
}
|
891
|
|
892
|
if ( this.headPos().y + height > this.initialHeadPos.y ) {
|
893
|
height = this.initialHeadPos.y - this.headPos().y;
|
894
|
}
|
895
|
var legLength = (this.body.leftLeg.length + this.body.rightLeg.length)/2;
|
896
|
var length = legLength+height;
|
897
|
this.bendLeg( this.body.leftLeg, length );
|
898
|
this.bendLeg( this.body.rightLeg, length );
|
899
|
|
900
|
this.rootMesh.position.y += height;
|
901
|
this.changed();
|
902
|
}
|
903
|
|
904
|
/**
|
905
|
Crouch a bit
|
906
|
@param height how much
|
907
|
*/
|
908
|
crouch( height ) {
|
909
|
if ( height < 0.001 ) {
|
910
|
// ignoring anything less than 1mm
|
911
|
return;
|
912
|
}
|
913
|
|
914
|
var legLength = (this.body.leftLeg.length + this.body.rightLeg.length)/2;
|
915
|
if ( legLength - height < 0.1 ) {
|
916
|
height = legLength - 0.1;
|
917
|
}
|
918
|
var length = legLength-height;
|
919
|
|
920
|
this.bendLeg( this.body.leftLeg, length );
|
921
|
this.bendLeg( this.body.rightLeg, length );
|
922
|
|
923
|
this.rootMesh.position.y -= height;
|
924
|
this.changed();
|
925
|
}
|
926
|
|
927
|
/**
|
928
|
Bend/stretch leg to a length
|
929
|
@param leg
|
930
|
@param length
|
931
|
*/
|
932
|
bendLeg( leg, length ) {
|
933
|
if ( length < 0 ) {
|
934
|
console.log("ERROR: can't bend leg to "+length);
|
935
|
return
|
936
|
}
|
937
|
var upper = this.skeleton.bones[leg.upper];
|
938
|
var lower = this.skeleton.bones[leg.lower];
|
939
|
var scaling = this.rootMesh.scaling.x;
|
940
|
|
941
|
if ( length > leg.lowerLength + leg.upperLength ) {
|
942
|
length = leg.lowerLength + leg.upperLength;
|
943
|
if ( length == leg.length ) {
|
944
|
return;
|
945
|
}
|
946
|
}
|
947
|
leg.length = length;
|
948
|
|
949
|
if ( ! leg.upperQuat ) {
|
950
|
leg.upperQuat = BABYLON.Quaternion.FromRotationMatrix(upper.getWorldMatrix().getRotationMatrix());
|
951
|
leg.upperQuatInv = BABYLON.Quaternion.Inverse(leg.upperQuat);
|
952
|
|
953
|
leg.lowerQuat = BABYLON.Quaternion.FromRotationMatrix(lower.getWorldMatrix().getRotationMatrix());
|
954
|
leg.lowerQuatInv = BABYLON.Quaternion.Inverse(leg.lowerQuat);
|
955
|
|
956
|
leg.upperRot = upper.getTransformNode().rotationQuaternion.clone();
|
957
|
}
|
958
|
|
959
|
// simplified math by using same length for both bones
|
960
|
// it's right angle, hypotenuse is bone
|
961
|
// length/2 is sinus of half of elbow angle
|
962
|
var boneLength = (leg.lowerLength + leg.upperLength)/2;
|
963
|
var innerAngle = Math.asin(length/2/boneLength);
|
964
|
//this.log("Bone length: "+boneLength+" distance to target "+length);
|
965
|
var upperAngle = Math.PI/2-innerAngle;
|
966
|
var lowerAngle = upperAngle*2;
|
967
|
|
968
|
var axis = leg.frontAxis.axis;
|
969
|
var sign = leg.frontAxis.sign;
|
970
|
|
971
|
var upperQuat = BABYLON.Quaternion.RotationAxis(axis,upperAngle*sign);
|
972
|
|
973
|
//var upperRot = upper.getTransformNode().rotationQuaternion;
|
974
|
upper.getTransformNode().rotationQuaternion = leg.upperRot.multiply(upperQuat);
|
975
|
|
976
|
var fix = leg.upperQuat.multiply(leg.lowerQuatInv);
|
977
|
var lowerQuat = BABYLON.Quaternion.RotationAxis(axis,-lowerAngle*sign);
|
978
|
lowerQuat = lowerQuat.multiply(fix);
|
979
|
|
980
|
lower.getTransformNode().rotationQuaternion = lowerQuat;
|
981
|
|
982
|
return length;
|
983
|
}
|
984
|
|
985
|
/**
|
986
|
Returns lenght of an arm or leg, in absolute world coordinates.
|
987
|
@param limb an arm or leg
|
988
|
@returns total length of lower and upper arm/leg
|
989
|
*/
|
990
|
calcLength(limb) {
|
991
|
var upper = this.skeleton.bones[limb.upper];
|
992
|
var lower = this.skeleton.bones[limb.lower];
|
993
|
var scaling = this.rootMesh.scaling.x;
|
994
|
limb.upperLength = upper.getAbsolutePosition().subtract(lower.getAbsolutePosition()).length()*scaling;
|
995
|
if ( lower.children && lower.children[0] ) {
|
996
|
limb.lowerLength = lower.getAbsolutePosition().subtract(lower.children[0].getAbsolutePosition()).length()*scaling;
|
997
|
} else {
|
998
|
limb.lowerLength = 0;
|
999
|
}
|
1000
|
limb.length = limb.upperLength+limb.lowerLength;
|
1001
|
this.log("Length of "+upper.name+": "+limb.upperLength+", "+lower.name+": "+limb.lowerLength);
|
1002
|
}
|
1003
|
|
1004
|
/**
|
1005
|
Returns total weight of a vector, x+y+z
|
1006
|
@param vector Vector3 to sum
|
1007
|
*/
|
1008
|
sum(vector) {
|
1009
|
return vector.x+vector.y+vector.z;
|
1010
|
}
|
1011
|
|
1012
|
// FIXME
|
1013
|
rotateBoneTo(bone, axis, angle) {
|
1014
|
var rotationMatrix = BABYLON.Matrix.RotationAxis(axis,angle);
|
1015
|
var rotated = BABYLON.Quaternion.FromRotationMatrix(rotationMatrix);
|
1016
|
bone.setRotationQuaternion(rotated);
|
1017
|
}
|
1018
|
|
1019
|
rotateBoneFor(bone, axis, increment) {
|
1020
|
var rotationMatrix = BABYLON.Matrix.RotationAxis(axis,increment);
|
1021
|
var quat = bone.rotationQuaternion;
|
1022
|
var rotated = BABYLON.Quaternion.FromRotationMatrix(rotationMatrix);
|
1023
|
bone.setRotationQuaternion(quat.multiply(rotated));
|
1024
|
}
|
1025
|
|
1026
|
guessArmsRotations() {
|
1027
|
|
1028
|
var leftUpperArm = this.skeleton.bones[this.body.leftArm.upper];
|
1029
|
var leftLowerArm = this.skeleton.bones[this.body.leftArm.lower];
|
1030
|
var rightUpperArm = this.skeleton.bones[this.body.rightArm.upper];
|
1031
|
var rightLowerArm = this.skeleton.bones[this.body.rightArm.lower];
|
1032
|
|
1033
|
// heuristics, assume both arm rotate around same rotation axis
|
1034
|
this.body.leftArm.sideAxis = this.guessRotation(leftUpperArm, BABYLON.Axis.Y);
|
1035
|
//this.body.rightArm.sideAxis = this.guessRotation(rightUpperArm, BABYLON.Axis.Y);
|
1036
|
this.body.rightArm.sideAxis = this.guessRotation(rightUpperArm, BABYLON.Axis.Y, this.body.leftArm.sideAxis.axis);
|
1037
|
|
1038
|
this.body.leftArm.frontAxis = this.guessRotation(leftUpperArm, BABYLON.Axis.Z);
|
1039
|
//this.body.rightArm.frontAxis = this.guessRotation(rightUpperArm, BABYLON.Axis.Z);
|
1040
|
this.body.rightArm.frontAxis = this.guessRotation(rightUpperArm, BABYLON.Axis.Z, this.body.leftArm.frontAxis.axis);
|
1041
|
|
1042
|
//this.debugViewer1 = new BABYLON.Debug.BoneAxesViewer(scene, leftUpperArm, this.rootMesh);
|
1043
|
|
1044
|
this.log("Left arm axis, side: "+this.body.leftArm.sideAxis.sign + this.body.leftArm.sideAxis.axis);
|
1045
|
this.log("Left arm axis, front: "+this.body.leftArm.frontAxis.sign + this.body.leftArm.frontAxis.axis);
|
1046
|
this.log("Right arm axis, side: "+this.body.rightArm.sideAxis.sign + this.body.rightArm.sideAxis.axis);
|
1047
|
this.log("Right arm axis, front: "+this.body.rightArm.frontAxis.sign + this.body.rightArm.frontAxis.axis);
|
1048
|
|
1049
|
this.body.leftArm.upperRot = BABYLON.Quaternion.FromRotationMatrix(leftUpperArm.getRotationMatrix());
|
1050
|
this.body.leftArm.lowerRot = BABYLON.Quaternion.FromRotationMatrix(leftLowerArm.getRotationMatrix());
|
1051
|
|
1052
|
this.body.rightArm.upperRot = BABYLON.Quaternion.FromRotationMatrix(rightUpperArm.getRotationMatrix());
|
1053
|
this.body.rightArm.lowerRot = BABYLON.Quaternion.FromRotationMatrix(rightLowerArm.getRotationMatrix());
|
1054
|
}
|
1055
|
|
1056
|
guessLegsRotations() {
|
1057
|
|
1058
|
var leftUpperLeg = this.skeleton.bones[this.body.leftLeg.upper];
|
1059
|
var leftLowerLeg = this.skeleton.bones[this.body.leftLeg.lower];
|
1060
|
var rightUpperLeg = this.skeleton.bones[this.body.rightLeg.upper];
|
1061
|
var rightLowerLeg = this.skeleton.bones[this.body.rightLeg.lower];
|
1062
|
|
1063
|
//this.debugViewer1 = new BABYLON.Debug.BoneAxesViewer(scene, leftUpperLeg, this.rootMesh);
|
1064
|
//this.debugViewer2 = new BABYLON.Debug.BoneAxesViewer(scene, leftLowerLeg, this.rootMesh);
|
1065
|
|
1066
|
this.body.leftLeg.frontAxis = this.guessRotation(leftUpperLeg, BABYLON.Axis.Z);
|
1067
|
this.body.rightLeg.frontAxis = this.guessRotation(rightUpperLeg, BABYLON.Axis.Z, this.body.leftLeg.frontAxis.axis);
|
1068
|
|
1069
|
//this.log("Left leg axis, front: "+this.body.leftLeg.frontAxis.sign + this.body.leftLeg.frontAxis.axis);
|
1070
|
|
1071
|
this.body.leftLeg.upperRot = BABYLON.Quaternion.FromRotationMatrix(leftUpperLeg.getRotationMatrix());
|
1072
|
this.body.leftLeg.lowerRot = BABYLON.Quaternion.FromRotationMatrix(leftLowerLeg.getRotationMatrix());
|
1073
|
|
1074
|
this.body.rightLeg.upperRot = BABYLON.Quaternion.FromRotationMatrix(rightUpperLeg.getRotationMatrix());
|
1075
|
this.body.rightLeg.lowerRot = BABYLON.Quaternion.FromRotationMatrix(rightLowerLeg.getRotationMatrix());
|
1076
|
}
|
1077
|
|
1078
|
guessRotation(bone, maxAxis, rotationAxis) {
|
1079
|
var axes = [BABYLON.Axis.X,BABYLON.Axis.Y,BABYLON.Axis.Z];
|
1080
|
if ( rotationAxis ) {
|
1081
|
axes = [ rotationAxis ];
|
1082
|
}
|
1083
|
var angles = [Math.PI/2,-Math.PI/2];
|
1084
|
var axis;
|
1085
|
var angle;
|
1086
|
var max = 0;
|
1087
|
for ( var i = 0; i < axes.length; i++ ) {
|
1088
|
for ( var j = 0; j<angles.length; j++ ) {
|
1089
|
var ret = this.tryRotation(bone, axes[i], angles[j]).multiply(maxAxis);
|
1090
|
var result = ret.x+ret.y+ret.z;
|
1091
|
if ( result >= max ) {
|
1092
|
axis = axes[i];
|
1093
|
angle = angles[j];
|
1094
|
max = result;
|
1095
|
}
|
1096
|
}
|
1097
|
}
|
1098
|
//this.log("Got it: "+axis+" "+angle+" - "+max);
|
1099
|
return {axis:axis,sign:Math.sign(angle)};
|
1100
|
}
|
1101
|
|
1102
|
tryRotation(bone, axis, angle) {
|
1103
|
var target = bone.children[0];
|
1104
|
var original = bone.getRotationQuaternion();
|
1105
|
var oldPos = target.getAbsolutePosition();
|
1106
|
//var oldPos = target.getTransformNode().getAbsolutePosition();
|
1107
|
var rotationMatrix = BABYLON.Matrix.RotationAxis(axis,angle);
|
1108
|
var quat = bone.rotationQuaternion;
|
1109
|
var rotated = BABYLON.Quaternion.FromRotationMatrix(rotationMatrix);
|
1110
|
bone.setRotationQuaternion(quat.multiply(rotated));
|
1111
|
//this.scene.render(); // doesn't work in XR
|
1112
|
//bone.computeWorldMatrix(true); // not required
|
1113
|
bone.computeAbsoluteTransforms();
|
1114
|
var newPos = target.getAbsolutePosition();
|
1115
|
//var newPos = target.getTransformNode().getAbsolutePosition();
|
1116
|
bone.setRotationQuaternion(original);
|
1117
|
bone.computeAbsoluteTransforms();
|
1118
|
var ret = newPos.subtract(oldPos);
|
1119
|
this.log("Tried "+axis+" "+angle+" - "+ret.z+" "+bone.name);
|
1120
|
return ret;
|
1121
|
}
|
1122
|
|
1123
|
/**
|
1124
|
Converts rotation quaternion of a node to euler angles
|
1125
|
@param node
|
1126
|
@returns Vector3 containing rotation around x,y,z
|
1127
|
*/
|
1128
|
euler(node) {
|
1129
|
return node.rotationQuaternion.toEulerAngles();
|
1130
|
}
|
1131
|
|
1132
|
degrees(node) {
|
1133
|
var rot = euler(node);
|
1134
|
return toDegrees(rot);
|
1135
|
}
|
1136
|
|
1137
|
/**
|
1138
|
Converts euler radians to degrees
|
1139
|
@param rot Vector3 rotation around x,y,z
|
1140
|
@returns Vector3 containing degrees around x,y,z
|
1141
|
*/
|
1142
|
toDegrees(rot) {
|
1143
|
var ret = new BABYLON.Vector3();
|
1144
|
ret.x = rot.x * 180/Math.PI;
|
1145
|
ret.y = rot.y * 180/Math.PI;
|
1146
|
ret.z = rot.z * 180/Math.PI;
|
1147
|
return ret;
|
1148
|
}
|
1149
|
|
1150
|
countBones(bones) {
|
1151
|
if ( bones ) {
|
1152
|
this.bonesDepth++;
|
1153
|
for ( var i = 0; i < bones.length; i ++ ) {
|
1154
|
if ( ! this.bonesProcessed.includes( bones[i].name )) {
|
1155
|
this.boneProcessed(bones[i]);
|
1156
|
this.processBones(bones[i].children);
|
1157
|
}
|
1158
|
}
|
1159
|
}
|
1160
|
}
|
1161
|
processBones(bones) {
|
1162
|
if ( bones ) {
|
1163
|
this.bonesDepth++;
|
1164
|
for ( var i = 0; i < bones.length; i ++ ) {
|
1165
|
if ( ! this.bonesProcessed.includes( bones[i].name )) {
|
1166
|
this.boneProcessed(bones[i]);
|
1167
|
var boneName = bones[i].name.toLowerCase();
|
1168
|
if ( ! this.body.root && boneName.includes('rootjoint') ) {
|
1169
|
//this.body.root = bones[i].name;
|
1170
|
this.body.root = i;
|
1171
|
this.log("found root "+boneName+" at depth "+this.bonesDepth);
|
1172
|
this.processBones(bones[i].children);
|
1173
|
} else if ( ! this.body.hips && this.isHipsName(boneName) && bones[i].children.length >= 3) {
|
1174
|
//} else if ( ! this.body.hips && bones[i].children.length >= 3) {
|
1175
|
//this.body.hips = bones[i].name;
|
1176
|
this.body.hips = i;
|
1177
|
this.log("found hips "+boneName);
|
1178
|
this.processHips(bones[i].children);
|
1179
|
} else {
|
1180
|
this.processBones(bones[i].children);
|
1181
|
}
|
1182
|
}
|
1183
|
}
|
1184
|
this.bonesDepth--;
|
1185
|
}
|
1186
|
}
|
1187
|
|
1188
|
isHipsName(boneName) {
|
1189
|
return boneName.includes('pelvis') || boneName.includes('hip') || boneName.includes('spine') || boneName.includes('root');
|
1190
|
}
|
1191
|
|
1192
|
processHips( bones ) {
|
1193
|
// hips have two legs and spine attached, possibly something else
|
1194
|
// TODO rewrite this to find most probable candidates for legs
|
1195
|
for ( var i = 0; i < bones.length; i++ ) {
|
1196
|
var boneName = bones[i].name.toLowerCase();
|
1197
|
if ( boneName.includes("spine") || boneName.includes("body") ) {
|
1198
|
this.processSpine(bones[i]);
|
1199
|
} else if ( boneName.includes( 'left' ) || this.isLegName(boneName, 'l', bones[i].children) ) {
|
1200
|
// left leg/thigh/upLeg/upperLeg
|
1201
|
this.tryLeg(this.body.leftLeg, bones[i]);
|
1202
|
} else if ( boneName.includes( 'right' ) || this.isLegName(boneName, 'r', bones[i].children)) {
|
1203
|
// right leg/thigh/upLeg/upperLeg
|
1204
|
this.tryLeg(this.body.rightLeg, bones[i]);
|
1205
|
} else if ( bones[i].children.length >= 3 && this.isHipsName(boneName) ) {
|
1206
|
this.log("Don't know how to handle bone "+boneName+", assuming hips" );
|
1207
|
this.boneProcessed(bones[i]);
|
1208
|
this.processHips(bones[i].children);
|
1209
|
} else if ( bones[i].children.length > 0 ) {
|
1210
|
this.log("Don't know how to handle bone "+boneName+", assuming spine" );
|
1211
|
this.processSpine(bones[i]);
|
1212
|
} else {
|
1213
|
this.log("Don't know how to handle bone "+boneName );
|
1214
|
this.boneProcessed(bones[i]);
|
1215
|
}
|
1216
|
}
|
1217
|
}
|
1218
|
|
1219
|
isLegName(boneName, lr, children ) {
|
1220
|
return boneName.includes( lr+'leg' ) ||
|
1221
|
boneName.includes( lr+'_' ) ||
|
1222
|
boneName.includes( ' '+lr+' ' ) ||
|
1223
|
boneName.includes(lr+'thigh') ||
|
1224
|
boneName.includes(lr+'hip') ||
|
1225
|
( children.length > 0 && children[0].children.length > 0 && children[0].name.toLowerCase().includes(lr+"_") )
|
1226
|
}
|
1227
|
|
1228
|
tryLeg( leg, bone ) {
|
1229
|
if ( bone.name.toLowerCase().includes( 'thigh' ) || bone.name.toLowerCase().includes( 'leg' )) {
|
1230
|
this.processLeg(leg, bone);
|
1231
|
} else if (bone.children.length == 0 || bone.children.length == 1 && bone.children[0].children.length == 0) {
|
1232
|
this.log("Ignoring bone "+bone.name);
|
1233
|
this.boneProcessed(bone);
|
1234
|
} else if (bone.children.length == 1 && bone.children[0].children.length == 1 && bone.children[0].children[0].children.length == 0 ) {
|
1235
|
// children depth 2, assume leg (missing foot?)
|
1236
|
if ( leg.upper && leg.lower ) {
|
1237
|
this.log( "Ignoring 1-joint leg "+bone.name );
|
1238
|
this.boneProcessed(bone);
|
1239
|
} else {
|
1240
|
this.log( "Processing 1-joint leg "+bone.name );
|
1241
|
this.processLeg(leg, bone.children[0]);
|
1242
|
}
|
1243
|
} else {
|
1244
|
// butt?
|
1245
|
this.log("Don't know how to handle leg "+bone.name+", trying children");
|
1246
|
this.boneProcessed(bone);
|
1247
|
this.processLeg(leg, bone.children[0]);
|
1248
|
}
|
1249
|
}
|
1250
|
|
1251
|
processLeg( leg, bone ) {
|
1252
|
this.log("Processing leg "+bone.name);
|
1253
|
if ( leg.upper && leg.lower ) {
|
1254
|
this.log("WARNING: leg already exists");
|
1255
|
}
|
1256
|
this.boneProcessed(bone);
|
1257
|
leg.upper = this.skeleton.getBoneIndexByName(bone.name);
|
1258
|
bone = bone.children[0];
|
1259
|
this.boneProcessed(bone);
|
1260
|
leg.lower = this.skeleton.getBoneIndexByName(bone.name);
|
1261
|
if ( bone.children && bone.children[0] ) {
|
1262
|
// foot exists
|
1263
|
this.processFoot(leg, bone.children[0]);
|
1264
|
}
|
1265
|
}
|
1266
|
|
1267
|
processFoot( leg, bone ) {
|
1268
|
//this.log("Processing foot "+bone.name);
|
1269
|
this.boneProcessed(bone);
|
1270
|
leg.foot.push(this.skeleton.getBoneIndexByName(bone.name));
|
1271
|
if ( bone.children && bone.children.length == 1 ) {
|
1272
|
this.processFoot( leg, bone.children[0] );
|
1273
|
}
|
1274
|
}
|
1275
|
|
1276
|
processSpine(bone) {
|
1277
|
if ( !bone ) {
|
1278
|
return;
|
1279
|
}
|
1280
|
//this.log("Processing spine "+bone.name);
|
1281
|
// spine has at least one bone, usually 2-3,
|
1282
|
this.boneProcessed(bone);
|
1283
|
if ( bone.children.length == 1 ) {
|
1284
|
this.body.spine.push(this.skeleton.getBoneIndexByName(bone.name));
|
1285
|
this.processSpine(bone.children[0]);
|
1286
|
} else if (bone.children.length >= 3 && this.hasHeadAndShoulders(bone) ) {
|
1287
|
// process shoulders and neck, other joints to be ignored
|
1288
|
for ( var i = 0; i < bone.children.length; i++ ) {
|
1289
|
var boneName = bone.children[i].name.toLowerCase();
|
1290
|
if ( boneName.includes( "neck" ) || boneName.includes("head") || (boneName.includes( "collar" ) && !boneName.includes( "bone" ) && !boneName.includes("lcollar") && !boneName.includes("rcollar")) ) {
|
1291
|
if ( !boneName.includes("head") && bone.children[i].children.length > 2 ) {
|
1292
|
this.log("Neck "+boneName+" of "+bone.name+" has "+bone.children[i].children.length+" children, assuming arms" );
|
1293
|
this.processNeck( bone.children[i] );
|
1294
|
this.processSpine( bone.children[i] );
|
1295
|
} else if ( bone.name.toLowerCase().includes( "neck" ) && boneName.toLowerCase().includes("head") ) {
|
1296
|
this.log("Arms grow out from neck?!");
|
1297
|
this.processNeck( bone );
|
1298
|
} else {
|
1299
|
this.processNeck( bone.children[i] );
|
1300
|
}
|
1301
|
} else if (this.isArm(bone.children[i], boneName)) {
|
1302
|
if ( boneName.includes( "left" ) || this.isArmName(boneName, 'l') ) {
|
1303
|
this.processArms( this.body.leftArm, bone.children[i] );
|
1304
|
} else if ( boneName.includes( "right" ) || this.isArmName(boneName, 'r') ) {
|
1305
|
this.processArms( this.body.rightArm, bone.children[i] );
|
1306
|
} else {
|
1307
|
this.log("Don't know how to handle shoulder "+boneName);
|
1308
|
this.boneProcessed(bone.children[i]);
|
1309
|
}
|
1310
|
} else {
|
1311
|
this.log("Don't know how to handle bone "+boneName);
|
1312
|
}
|
1313
|
}
|
1314
|
} else if ( bone.name.toLowerCase().includes("breast")) {
|
1315
|
this.countBones(bone.children);
|
1316
|
} else {
|
1317
|
this.log("Not sure how to handle spine "+bone.name+", trying recursion");
|
1318
|
this.body.spine.push(this.skeleton.getBoneIndexByName(bone.name));
|
1319
|
this.processSpine(bone.children[0]);
|
1320
|
}
|
1321
|
}
|
1322
|
|
1323
|
isArmName(boneName, lr) {
|
1324
|
return boneName.includes( lr+'shoulder' ) ||
|
1325
|
boneName.includes( lr+'clavicle' ) ||
|
1326
|
boneName.includes( lr+'collar' ) ||
|
1327
|
boneName.includes( lr+'arm' ) ||
|
1328
|
boneName.includes( ' '+lr+' ' ) ||
|
1329
|
boneName.includes( lr+"_" );
|
1330
|
}
|
1331
|
|
1332
|
isArm( bone, boneName ) {
|
1333
|
//( ! boneName.includes("breast") && !boneName.includes("pistol")) {
|
1334
|
if ( boneName.includes("shoulder") || boneName.includes("clavicle") ) {
|
1335
|
return true;
|
1336
|
}
|
1337
|
return ( this.hasChildren(bone) && this.hasChildren(bone.children[0]) && this.hasChildren(bone.children[0].children[0]) )
|
1338
|
}
|
1339
|
|
1340
|
hasChildren( bone ) {
|
1341
|
return bone.children && bone.children.length > 0;
|
1342
|
}
|
1343
|
|
1344
|
hasHeadAndShoulders( bone ) {
|
1345
|
var count = 0;
|
1346
|
for ( var i = 0; i < bone.children.length; i ++ ) {
|
1347
|
if ( this.isHeadOrShoulder(bone.children[i]) ) {
|
1348
|
count++;
|
1349
|
}
|
1350
|
}
|
1351
|
this.log("Head and shoulders count: "+count+"/"+bone.children.length);
|
1352
|
return count >= 3;
|
1353
|
}
|
1354
|
|
1355
|
isHeadOrShoulder( bone ) {
|
1356
|
var boneName = bone.name.toLowerCase();
|
1357
|
return boneName.includes('head') || boneName.includes('neck') ||
|
1358
|
((bone.children && bone.children.length > 0)
|
1359
|
&& ( boneName.includes('shoulder')
|
1360
|
|| boneName.includes( 'clavicle' )
|
1361
|
|| boneName.includes( 'collar' )
|
1362
|
|| boneName.includes( 'arm' )
|
1363
|
));
|
1364
|
}
|
1365
|
|
1366
|
processNeck( bone ) {
|
1367
|
if ( this.body.neck.neck && this.bonesProcessed.includes(bone.name) ) {
|
1368
|
this.log("neck "+bone.name+" already processed: "+this.body.neck.neck);
|
1369
|
return;
|
1370
|
}
|
1371
|
this.log("processing neck "+bone.name+" children: "+bone.children.length);
|
1372
|
this.body.neck = this.skeleton.getBoneIndexByName(bone.name);
|
1373
|
this.boneProcessed(bone);
|
1374
|
var neck = bone;
|
1375
|
if ( bone.children && bone.children.length > 0 ) {
|
1376
|
bone = bone.children[0];
|
1377
|
} else {
|
1378
|
this.log("Missing head?!");
|
1379
|
}
|
1380
|
this.body.head = this.skeleton.getBoneIndexByName(bone.name);
|
1381
|
var head = bone;
|
1382
|
|
1383
|
var refHead = new BABYLON.Vector3();
|
1384
|
head.getDirectionToRef(BABYLON.Axis.Z,this.rootMesh,refHead);
|
1385
|
this.roundVector(refHead);
|
1386
|
this.log("RefZ head: "+refHead);
|
1387
|
|
1388
|
var refNeck = new BABYLON.Vector3();
|
1389
|
neck.getDirectionToRef(BABYLON.Axis.Z,this.rootMesh,refNeck);
|
1390
|
this.roundVector(refNeck);
|
1391
|
this.log("RefZ neck: "+refNeck);
|
1392
|
|
1393
|
// some characters have Z axis of neck and head pointing in opposite direction
|
1394
|
// (rotated around Y) causing rotation to point backwards,
|
1395
|
// they need different calculation
|
1396
|
this.headAxisFix = refHead.z * refNeck.z;
|
1397
|
|
1398
|
this.headQuat = BABYLON.Quaternion.FromRotationMatrix(head.getWorldMatrix().getRotationMatrix());
|
1399
|
this.headQuatInv = BABYLON.Quaternion.Inverse(this.headQuat);
|
1400
|
|
1401
|
this.neckQuat = BABYLON.Quaternion.FromRotationMatrix(neck.getWorldMatrix().getRotationMatrix());
|
1402
|
this.neckQuatInv = BABYLON.Quaternion.Inverse(this.neckQuat);
|
1403
|
|
1404
|
var target = new BABYLON.Vector3(0,0,1);
|
1405
|
target.rotateByQuaternionToRef(BABYLON.Quaternion.Inverse(this.rootMesh.rotationQuaternion),target);
|
1406
|
target.rotateByQuaternionToRef(this.headQuatInv,target);
|
1407
|
|
1408
|
this.headTarget = target.negate().normalizeToNew();
|
1409
|
|
1410
|
this.log("Head target: "+this.headTarget+" axisFix "+this.headAxisFix);
|
1411
|
|
1412
|
this.boneProcessed(bone);
|
1413
|
//this.processBones(bone.children);
|
1414
|
this.countBones(bone.children);
|
1415
|
}
|
1416
|
|
1417
|
processArms( arm, bone ) {
|
1418
|
this.log("Processing arm "+bone.name+" "+bone.getIndex()+" "+this.skeleton.getBoneIndexByName(bone.name));
|
1419
|
arm.shoulder = this.skeleton.getBoneIndexByName(bone.name);
|
1420
|
this.boneProcessed(bone);
|
1421
|
bone = bone.children[0];
|
1422
|
arm.upper = this.skeleton.getBoneIndexByName(bone.name);
|
1423
|
this.boneProcessed(bone);
|
1424
|
bone = bone.children[0];
|
1425
|
arm.lower = this.skeleton.getBoneIndexByName(bone.name);
|
1426
|
this.boneProcessed(bone);
|
1427
|
bone = bone.children[0];
|
1428
|
arm.hand = this.skeleton.getBoneIndexByName(bone.name);
|
1429
|
this.boneProcessed(bone);
|
1430
|
if ( bone.children ) {
|
1431
|
if ( bone.children.length == 5 ) {
|
1432
|
for ( var i = 0; i < bone.children.length; i++ ) {
|
1433
|
var boneName = bone.children[i].name.toLowerCase();
|
1434
|
if ( boneName.includes("index") || boneName.includes("point") ) {
|
1435
|
this.processFinger(arm.fingers.index, bone.children[i]);
|
1436
|
} else if (boneName.includes("middle")) {
|
1437
|
this.processFinger(arm.fingers.middle, bone.children[i]);
|
1438
|
} else if (boneName.includes("pink") || boneName.includes("little")) {
|
1439
|
this.processFinger(arm.fingers.pinky, bone.children[i]);
|
1440
|
} else if (boneName.includes("ring")) {
|
1441
|
this.processFinger(arm.fingers.ring, bone.children[i]);
|
1442
|
} else if (boneName.includes("thumb")) {
|
1443
|
this.processFinger(arm.fingers.thumb, bone.children[i]);
|
1444
|
} else {
|
1445
|
this.log("Can't process finger "+boneName);
|
1446
|
this.boneProcessed(bone.children[i]);
|
1447
|
}
|
1448
|
}
|
1449
|
} else {
|
1450
|
this.log("Can't process fingers of "+bone.name+" length: "+bone.children.length);
|
1451
|
}
|
1452
|
}
|
1453
|
}
|
1454
|
|
1455
|
processFinger( finger, bone ) {
|
1456
|
if ( bone ) {
|
1457
|
finger.push(this.skeleton.getBoneIndexByName(bone.name));
|
1458
|
this.boneProcessed(bone);
|
1459
|
if ( bone.children && bone.children.length > 0 ) {
|
1460
|
this.processFinger(finger,bone.children[0]);
|
1461
|
}
|
1462
|
}
|
1463
|
}
|
1464
|
|
1465
|
// unused, use only for debugging characters
|
1466
|
processAnimations(targeted) {
|
1467
|
var frames = [];
|
1468
|
for ( var j = 0; j < targeted.length; j++ ) {
|
1469
|
//this.log("animation: "+animations[j].animation.name+" target: "+animations[i].target.name);
|
1470
|
if ( !this.animationTargets.includes(targeted[j].target.name) ) {
|
1471
|
this.animationTargets.push(targeted[j].target.name);
|
1472
|
if ( ! this.bonesProcessed.includes(targeted[j].target.name) ) {
|
1473
|
this.log("Missing target "+targeted[j].target.name);
|
1474
|
}
|
1475
|
}
|
1476
|
var keys = targeted[j].animation.getKeys();
|
1477
|
for ( var i = 0; i < keys.length; i++ ) {
|
1478
|
// square complexity
|
1479
|
if ( ! frames.includes(keys[i].frame) ) {
|
1480
|
frames.push( keys[i].frame );
|
1481
|
}
|
1482
|
}
|
1483
|
}
|
1484
|
}
|
1485
|
|
1486
|
/**
|
1487
|
Start a given animation
|
1488
|
@param animationName animation to start
|
1489
|
*/
|
1490
|
startAnimation(animationName) {
|
1491
|
for ( var i = 0; i < this.getAnimationGroups().length; i++ ) {
|
1492
|
var group = this.getAnimationGroups()[i];
|
1493
|
if ( group.name == animationName ) {
|
1494
|
//this.log("Animation group: "+animationName);
|
1495
|
if ( group.isPlaying ) {
|
1496
|
group.pause();
|
1497
|
this.log("paused "+animationName);
|
1498
|
} else {
|
1499
|
if ( this.fixes ) {
|
1500
|
if (typeof this.fixes.beforeAnimation !== 'undefined' ) {
|
1501
|
this.log( "Applying fixes for: "+this.folder.name+" beforeAnimation: "+this.fixes.beforeAnimation);
|
1502
|
this.groundLevel( this.fixes.beforeAnimation );
|
1503
|
}
|
1504
|
this.disableNodes();
|
1505
|
if (typeof this.fixes.before !== 'undefined' ) {
|
1506
|
this.fixes.before.forEach( obj => {
|
1507
|
if ( animationName == obj.animation && obj.enableNodes ) {
|
1508
|
console.log(obj);
|
1509
|
this.enableNodes(obj.enableNodes, true);
|
1510
|
}
|
1511
|
});
|
1512
|
}
|
1513
|
}
|
1514
|
this.jump(0);
|
1515
|
group.play(group.loopAnimation);
|
1516
|
this.log("playing "+animationName);
|
1517
|
this.activeAnimation = animationName;
|
1518
|
}
|
1519
|
} else if ( group.isPlaying ) {
|
1520
|
// stop all other animations
|
1521
|
group.pause();
|
1522
|
group.reset();
|
1523
|
}
|
1524
|
}
|
1525
|
}
|
1526
|
|
1527
|
/**
|
1528
|
Adds or remove all avatar meshes to given ShadowGenerator.
|
1529
|
@param shadowGenerator removes shadows if null
|
1530
|
*/
|
1531
|
castShadows( shadowGenerator ) {
|
1532
|
if ( this.character && this.character.meshes ) {
|
1533
|
for ( var i = 0; i < this.character.meshes.length; i++ ) {
|
1534
|
if (shadowGenerator) {
|
1535
|
shadowGenerator.getShadowMap().renderList.push(this.character.meshes[i]);
|
1536
|
} else if ( this.shadowGenerator ) {
|
1537
|
var index = this.shadowGenerator.getShadowMap().renderList.indexOf(this.character.meshes[i]);
|
1538
|
if ( index >= 0 ) {
|
1539
|
this.shadowGenerator.getShadowMap().renderList.splice(index,1);
|
1540
|
}
|
1541
|
}
|
1542
|
}
|
1543
|
}
|
1544
|
this.shadowGenerator = shadowGenerator;
|
1545
|
}
|
1546
|
|
1547
|
/**
|
1548
|
Resize the avatar taking into account userHeight and headPos.
|
1549
|
*/
|
1550
|
resize() {
|
1551
|
var oldScale = this.rootMesh.scaling.y;
|
1552
|
var oldHeadPos = this.headPos();
|
1553
|
var scale = oldScale*this.userHeight/oldHeadPos.y;
|
1554
|
this.rootMesh.scaling = new BABYLON.Vector3(scale,scale,scale);
|
1555
|
//this.rootMesh.computeWorldMatrix(true);
|
1556
|
//this.scene.render();
|
1557
|
this.initialHeadPos = this.headPos();
|
1558
|
this.log("Rescaling from "+oldScale+ " to "+scale+", head position from "+oldHeadPos+" to "+this.initialHeadPos);
|
1559
|
this.changed();
|
1560
|
return scale;
|
1561
|
}
|
1562
|
|
1563
|
/**
|
1564
|
Set the name and display it above the avatar
|
1565
|
@param name
|
1566
|
*/
|
1567
|
async setName(name) {
|
1568
|
this.writer.clear(this.parentMesh);
|
1569
|
//this.writer.relativePosition = this.headPos().add(new BABYLON.Vector3(0,.4,0));
|
1570
|
this.writer.relativePosition = this.rootMesh.position.add(new BABYLON.Vector3(0,.4+this.height(),0));
|
1571
|
this.writer.write(this.parentMesh, name);
|
1572
|
this.name = name;
|
1573
|
}
|
1574
|
|
1575
|
/** Called when avatar size/height changes, TODO notify listeners */
|
1576
|
changed() {
|
1577
|
if ( this.nameParent ) {
|
1578
|
var pos = this.headPos().clone();
|
1579
|
pos.y += .4;
|
1580
|
this.nameParent.position = pos;
|
1581
|
}
|
1582
|
}
|
1583
|
|
1584
|
async wrote(client) {
|
1585
|
console.log(client);
|
1586
|
var limit = 20;
|
1587
|
var text = [this.name];
|
1588
|
var line = '';
|
1589
|
client.wrote.split(' ').forEach((word) => {
|
1590
|
if ( line.length + word.length > limit ) {
|
1591
|
text.push(line);
|
1592
|
line = '';
|
1593
|
}
|
1594
|
line += word + ' ';
|
1595
|
});
|
1596
|
text.push(line);
|
1597
|
console.log(text);
|
1598
|
|
1599
|
this.writer.clear(this.parentMesh);
|
1600
|
//this.writer.relativePosition = this.headPos().add(new BABYLON.Vector3(0,.4+.2*(text.length-1),0));
|
1601
|
this.writer.relativePosition = this.rootMesh.position.add(new BABYLON.Vector3(0,.4+this.height()+.2*(text.length-1),0));
|
1602
|
this.writer.writeArray(this.parentMesh, text);
|
1603
|
}
|
1604
|
|
1605
|
}
|