Project

General

Profile

Bug #125 » avatar.js

Vander Dias, 06/09/2022 01:29 PM

 
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
}
(2-2/2)