repo: janusweb
action: commit
revision: 
path_from: 
revision_from: b4a55df88a9b5484b51dd580c45747abf37fadfd:
path_to: 
revision_to: 
git.thebackupbox.net
janusweb
git clone git://git.thebackupbox.net/janusweb
commit b4a55df88a9b5484b51dd580c45747abf37fadfd
Author: James Baicoianu 
Date:   Mon Sep 20 15:23:39 2021 -0700

    Improved avatar animations and fading

diff --git a/scripts/janusghost.js b/scripts/janusghost.js
index 657f509fde5aac4f2f3cc04eff2c3c3f2e19a3c6..
index ..963b4c4b953a3c04a6e3a44c4a0ef31785b1c1eb 100644
--- a/scripts/janusghost.js
+++ b/scripts/janusghost.js
@@ -1,4 +1,5 @@
 elation.require(['janusweb.janusbase', 'engine.things.leapmotion'], function() {
+
   elation.component.add('engine.things.janusghost', function() {
     this.postinit = function() {
       elation.engine.things.janusghost.extendclass.postinit.call(this);
@@ -19,6 +20,23 @@ elation.require(['janusweb.janusbase', 'engine.things.leapmotion'], function() {
       });

       this.frames = false;
+
+      if (!('project_vertex_discard_close' in THREE.ShaderChunk)) {
+        THREE.ShaderChunk['color_fragment_discard_close'] = `
+          #if defined( USE_COLOR_ALPHA )
+            diffuseColor *= vColor;
+          #elif defined( USE_COLOR )
+            diffuseColor.rgb *= vColor;
+          #endif
+          float dist = length(vViewPosition);
+          float mindist = .3;
+          float maxdist = .5;
+          if (dist < maxdist) {
+            diffuseColor.a = clamp(((dist - mindist) / (maxdist - mindist)), 0., 1.);
+            //discard;
+          }
+        `;
+      }
     }
     this.createObject3D = function() {
       if (this.ghost_src) {
@@ -114,6 +132,45 @@ elation.require(['janusweb.janusbase', 'engine.things.leapmotion'], function() {
         console.log(this.ghostassets);
         this.assetpack.loadJSON(assets.assetlist);
       }
+
+      let animnames = []; //'idle', 'walk', 'walk_left', 'walk_right', 'walk_back', 'run', 'jump', 'fly', 'speak', 'type', 'portal'];
+      let animassets = assets.assetlist.filter(asset => animnames.indexOf(asset.name) != -1);
+      console.log('some ghost animations', animassets, assets);
+
+      if (!this.animationmixer) {
+        // Set up our animation mixer with a simple bone mapper for our head.  We'll add more animations to this ass other assets load
+        // TODO - this is probably also where we'd map any other tracked objects (hands, hips, etc). and set up IK
+
+/*
+        let headtrack = new THREE.QuaternionKeyframeTrack('Neck.quaternion', [0], [0, 0, 0, 1]),
+            headclip = new THREE.AnimationClip('head_rotation', -1, [headtrack]);
+        this.headtrack = headtrack;
+
+        this.initAnimations([ headclip ]);
+        //this.animations['head_rotation'].play();
+*/
+        this.initAnimations([]);
+      }
+
+      animassets.forEach(anim => {
+        let asset = this.getAsset('model', anim.name);
+        console.log('try to load the animation', asset, anim);
+        if (asset) {
+          if (!asset.loaded) {
+            let model = asset.getInstance();
+            elation.events.add(asset, 'asset_load', ev => {
+              let clip = false;
+              model.traverse(n => { if (n.animations && n.animations.length > 0) clip = n.animations[0]; });
+              if (clip) {
+                if (this.animationmixer && !this.animations[anim.name]) {
+                  let action = this.animationmixer.clipAction(clip);
+                  this.animations[anim.name] = action;
+                }
+              }
+            });
+          }
+        }
+      });
     }
     this.getGhostObjects = function() {
       var objects = {};
@@ -217,6 +274,9 @@ elation.require(['janusweb.janusbase', 'engine.things.leapmotion'], function() {
             //rotation: V(0, 180, 0),
             lighting: this.lighting,
             //cull_face: 'none'
+            shader_chunk_replace: {
+              'color_fragment': 'color_fragment_discard_close',
+            },
           });
         } else {
           this.body = bodyid;
@@ -225,8 +285,140 @@ elation.require(['janusweb.janusbase', 'engine.things.leapmotion'], function() {
         if (pos) this.body.pos = pos;
         this.body.start();
         if (scale && this.body) this.body.scale.fromArray(scale);
+
+        setTimeout(() => {
+          // FIXME - there's definitely a better way to do this, but without a timeout this runs before the modelasset is initialized
+          if (this.body.modelasset) {
+            if (this.body.modelasset.loaded) {
+              this.loadAnimations();
+            } else {
+              elation.events.add(this.body.modelasset, 'asset_load_complete', () => this.loadAnimations());
+            }
+          }
+        }, 100);
+      }
+    }
+    this.loadAnimations = function() {
+      let animasset = this.getAsset('model', 'avatar_animations');
+      console.log('my anim asset', animasset);
+      if (!animasset.loaded) {
+        console.log('load the asset');
+        elation.events.add(animasset, 'asset_load_complete', ev => {
+          console.log('asset loaded', animasset.animations);
+          this.cloneAnimations(animasset);
+        });
+        animasset.load();
+      } else {
+        console.log('asset is loaded', animasset.animations);
+      }
+    }
+    this.cloneAnimations = function(animasset) {
+      let animations = animasset.animations;
+      //console.log('clone all the animations', animations, animasset._model);
+      if (this.body) {
+        this.initHeadAnimation(animasset);
+
+        animations.forEach(clip => {
+          this.body.animations[clip.name] = this.body.animationmixer.clipAction(this.retargetAnimation(clip, animasset._model));
+          //console.log('new clip', clip.name, clip, this.body.animations[clip.name]);
+        });
+        //console.log('head rot', this.body.animations['head_rotation'], headclip, headaction);
+        //console.log(this.body.modelasset, this.body.modelasset.vrm);
+        if (this.body.modelasset && this.body.modelasset.vrm) {
+          let rename = {};
+          let bonemap = this.body.modelasset.vrm.humanoid.humanBones;
+          for (let k in bonemap) {
+            console.log(k, bonemap[k]);
+            if (bonemap[k].length > 0) {
+              rename[bonemap[k][0].node.name] = k;
+            }
+          }
+          let bones = [];
+          let meshes = [];
+          this.body.objects['3d'].traverse(n => { if (n instanceof THREE.Bone) bones.push(n); else if (n instanceof THREE.SkinnedMesh) meshes.push(n); });
+          /*
+          console.log('rename the bones!', rename);
+          THREE.SkeletonUtils.renameBones(bones, rename);
+          console.log('bones now', bones);
+          */
+          console.log('retarget the clips!', meshes, animations);
+          
+        }
       }
     }
+    this.retargetAnimation = function(clip, sourcecontainer) {
+      let newclip = clip.clone();
+      let sourcemesh = null,
+          destmesh = null;
+      if (sourcecontainer) {
+        sourcecontainer.traverse(n => { /*console.log(' - ', n); */ if (n instanceof THREE.SkinnedMesh && n.skeleton) sourcemesh = n; });
+      }
+      this.objects['3d'].traverse(n => { if (n instanceof THREE.SkinnedMesh && n.skeleton) destmesh = n; });
+      //console.log('RETARGET ANI*MATION', clip, sourcemesh, destmesh, sourcecontainer);
+      let remove = [];
+      if (this.body && this.body.modelasset && this.body.modelasset.vrm) {
+        let vrm = this.body.modelasset.vrm;
+        //console.log('RETARGET ANI*MATION', clip, this.body.modelasset.vrm, sourcemesh, destmesh);
+        newclip.tracks.forEach(track => {
+          let parts = track.name.split('.');
+          console.log(parts, track.name);
+          try {
+            let bone = vrm.humanoid.getBone(parts[0]);
+            if (bone) {
+              track.name = bone.node.name + '.' + parts[1];
+              let sourcebone = sourcemesh.skeleton.getBone(bone.node.name);
+              //console.log(track, bone);
+            } else {
+              //console.log('no bone!', parts, track);
+            }
+          } catch (e) {
+            console.log('omgwtf', e.message);
+            remove.push(track);
+          }
+        });
+        remove.forEach(track => {
+          let idx = newclip.tracks.indexOf(track);
+          if (idx != -1) {
+            newclip.tracks.splice(idx, 1);
+            //console.log('removed track', track);
+          }
+        });
+      } else if (destmesh && sourcemesh) {
+        // FIXME - silly hack to store skeleton, we should just be extracting it at load time
+        this.body.skeleton = destmesh.skeleton;
+        this.body.objects['3d'].skeleton = destmesh.skeleton;
+        //newclip = THREE.SkeletonUtils.retargetClip(destmesh, sourcemesh, newclip, {useFirstFramePosition: true});
+        //console.log('retarget it!', newclip, destmesh, sourcemesh);
+        newclip.tracks.forEach(track => {
+          let [name, property] = track.name.split('.');
+          let srcbone = sourcemesh.skeleton.getBoneByName(name);
+          let dstbone = destmesh.skeleton.getBoneByName(name);
+          //console.log(' - fix track', name, property, track, srcbone, dstbone);
+          if (dstbone) {
+            let scale = srcbone.position.length() / dstbone.position.length();
+            if (property == 'position') {
+              for (let i = 0; i < track.values.length; i++) {
+                track.values[i] /= scale;
+              }
+              //remove.push(track);
+            } else if (property == 'quaternion') {
+            }
+          } else {
+            //console.log('missing bone!', srcbone, track);
+            remove.push(track);
+          }
+        });
+      }
+      remove.forEach(track => {
+        let idx = newclip.tracks.indexOf(track);
+        if (idx != -1) {
+          newclip.tracks.splice(idx, 1);
+          //console.log('removed track', track);
+        }
+      });
+      //console.log('finished clip', newclip);
+      return newclip;
+    }
     this.rebindAnimations = function() {
       this.body.rebindAnimations();
     }
@@ -289,7 +481,10 @@ elation.require(['janusweb.janusbase', 'engine.things.leapmotion'], function() {

             matrix.makeBasis(xdir, ydir, zdir);
             q1.setFromRotationMatrix(matrix);
-            this.head.properties.orientation.copy(this.orientation).inverse().multiply(q1);
+            this.head.properties.orientation.copy(this.orientation).invert().multiply(q1);
+            if (this.body && this.headaction) {
+              this.setHeadOrientation(this.head.orientation);
+            }
             if (movedata.head_pos && this.face) {
               var headpos = this.head.properties.position;
               var facepos = this.face.properties.position;
@@ -357,6 +552,20 @@ elation.require(['janusweb.janusbase', 'engine.things.leapmotion'], function() {
         if (movedata.userid_pos) {
           this.userid_pos = movedata.userid_pos;
         }
+
+        // FIXME - workaround for null values
+        if (isNaN(this.position.x)) this.position.x = 0;
+        if (isNaN(this.position.y)) this.position.y = 0;
+        if (isNaN(this.position.z)) this.position.z = 0;
+        if (isNaN(this.orientation.x) || isNaN(this.orientation.y) || isNaN(this.orientation.z) || isNaN(this.orientation.w)) this.orientation.set(0,0,0,1);
+
+        if (this.head) {
+          if (isNaN(this.head.position.x)) this.head.position.x = 0;
+          if (isNaN(this.head.position.y)) this.head.position.y = 0;
+          if (isNaN(this.head.position.z)) this.head.position.z = 0;
+          if (isNaN(this.head.orientation.x) || isNaN(this.head.orientation.y) || isNaN(this.head.orientation.z) || isNaN(this.head.orientation.w)) this.head.orientation.set(0,0,0,1);
+        }
+
         this.objects.dynamics.updateState();
         this.refresh();
       }
@@ -431,7 +640,7 @@ elation.require(['janusweb.janusbase', 'engine.things.leapmotion'], function() {
       }

       var inverse = new THREE.Matrix4();
-      inverse.getInverse(this.objects['3d'].matrixWorld);
+      inverse.copy(this.objects['3d'].matrixWorld).invert();
       if (hand0 && hand0.state) {
         this.hands.left.show();
         this.hands.left.setState(hand0.state, inverse);
@@ -460,6 +669,7 @@ elation.require(['janusweb.janusbase', 'engine.things.leapmotion'], function() {
       } 
     }
     this.updateTransparency = function() {
+return;
       var player = this.engine.client.player;

       var dist = player.distanceTo(this);
@@ -540,5 +750,35 @@ elation.require(['janusweb.janusbase', 'engine.things.leapmotion'], function() {
         });
       }
     }
+    this.setAnimation = function(anim) {
+      if (this.body) this.body.anim_id = anim;
+    }
+    this.initHeadAnimation = function(animasset) {
+      let body = this.body || this._target.body;
+      if (body && !this.headaction) {
+        if (!body.animationmixer) {
+          body.initAnimations([]);
+        }
+        // Set up our animation mixer with a simple bone mapper for our head.  We'll add more animations to this as other assets load
+        // TODO - this is probably also where we'd map any other tracked objects (hands, hips, etc). and set up IK
+
+        let headtrack = new THREE.QuaternionKeyframeTrack('Head.quaternion', [0], [0, 0, 0, 1]),
+            headclip = new THREE.AnimationClip('head_rotation', -1, [headtrack]);
+        let headaction = body.animationmixer.clipAction(this.retargetAnimation(headclip, animasset._model));
+        headaction.weight = 2;
+        this.headaction = headaction;
+        headaction.play();
+      }
+    }
+    this.setHeadOrientation = function(orientation, invert) {
+      let headaction = this.headaction || this._target.headaction;
+      if (headaction) {
+        let track = headaction._clip.tracks[0];
+        track.values[0] = orientation.x * (invert ? -1 : 1);
+        track.values[1] = orientation.y * (invert ? -1 : 1);
+        track.values[2] = orientation.z * (invert ? -1 : 1);
+        track.values[3] = orientation.w; // * (invert ? -1 : 1);
+      }
+    }
   }, elation.engine.things.janusbase);
 });
diff --git a/scripts/janusplayer.js b/scripts/janusplayer.js
index f37c8119b5ac880aa4848fd5d56533bc866dde60..
index ..72f0292bed81ac842c2bea294ec936c82f92c86b 100644
--- a/scripts/janusplayer.js
+++ b/scripts/janusplayer.js
@@ -2,7 +2,18 @@ elation.require(['engine.things.player', 'janusweb.external.JanusVOIP', 'ui.butt
   elation.requireCSS('janusweb.janusplayer');

   elation.component.add('engine.things.janusplayer', function() {
-    this.defaultavatar = '\n  \n    \n  \n  \n    \n      \n    \n  \n'
+    //this.defaultavatar = '\n  \n    \n  \n  \n    \n      \n    \n  \n'
+    this.defaultavatar = `
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    `;

     this.postinit = function() {
       elation.engine.things.janusplayer.extendclass.postinit.call(this);
@@ -15,9 +26,11 @@ elation.require(['engine.things.player', 'janusweb.external.JanusVOIP', 'ui.butt
         cursor_visible: {type: 'boolean', default: true, set: this.toggleCursorVisibility},
         cursor_opacity: {type: 'float', default: 1.0, set: this.toggleCursorVisibility},
         usevoip: {type: 'boolean', default: false },
+        defaultanimation: {type: 'string', default: 'idle' },
         collision_radius: {type: 'float', default: .25, set: this.updateCollider},
         party_mode: { type: 'boolean', set: this.updatePartyMode },
         avatarsrc: { type: 'string' },
+        cameraview: { type: 'string', default: 'firstperson' },
       });

       var controllerconfig = this.getSetting('controls.settings');
@@ -32,7 +45,7 @@ elation.require(['engine.things.player', 'janusweb.external.JanusVOIP', 'ui.butt
       elation.events.add(this.engine.client.view.container, 'touchend', elation.bind(this, this.handleTouchEnd));

       this.controlstate2 = this.engine.systems.controls.addContext('janusplayer', {
-        'voip_active': ['keyboard_v,keyboard_shift_v', elation.bind(this, this.activateVOIP)],
+        'toggle_view': ['keyboard_v,keyboard_shift_v', elation.bind(this, this.toggleCamera)],
         //'browse_back': ['gamepad_any_button_4', elation.bind(this, this.browseBack)],
         //'browse_forward': ['gamepad_any_button_5', elation.bind(this, this.browseForward)],
       });
@@ -122,11 +135,13 @@ elation.require(['engine.things.player', 'janusweb.external.JanusVOIP', 'ui.butt

       this.getAvatarData().then(avatar => {;
         if (avatar && false) { // FIXME - self avatar is buggy so it's disabled
+/*
           this.ghost = this.createObject('ghost', {
             ghost_id: this.getUsername(),
             avatar_src: 'data:text/plain,' + encodeURIComponent(avatar),
             showlabel: false
           });
+*/
         }
       });

@@ -265,6 +280,7 @@ elation.require(['engine.things.player', 'janusweb.external.JanusVOIP', 'ui.butt
           }
         }
         if (this.ghost) {
+          this.ghost.setHeadOrientation(this.head.orientation, true);
           if (this.ghost._target.head) {
             //this.ghost._target.face.position.copy(this.head.position);
             this.ghost.head.orientation.copy(this.head.orientation);
@@ -439,15 +455,16 @@ elation.require(['engine.things.player', 'janusweb.external.JanusVOIP', 'ui.butt
         this.ghost.die();
         this.ghost = false;
         this.visible = false;
-      } else if (!this.ghost && this.room.selfavatar) {
+      } else if (!this.ghost) { // && this.room.selfavatar) {
         // FIXME - self avatar is buggy so it's disabled
         this.getAvatarData().then(avatar => {
           if (avatar) {
             this.ghost = this.createObject('ghost', {
               ghost_id: this.getUsername(),
               avatar_src: 'data:text/plain,' + encodeURIComponent(avatar),
-              showlabel: false
+              showlabel: false,
             });
+            this.ghost.orientation.set(0,1,0,0);
           }
         });
         this.visible = true;
@@ -553,6 +570,9 @@ elation.require(['engine.things.player', 'janusweb.external.JanusVOIP', 'ui.butt

         collision_radius: ['property', 'collision_radius'],

+        currentavatar: ['property', 'currentavatar'],
+        defaultanimation: ['property', 'defaultanimation'],
+
         localToWorld:  ['function', 'localToWorld'],
         worldToLocal:  ['function', 'worldToLocal'],
         appendChild:   ['function', 'appendChild'],
@@ -604,11 +624,13 @@ elation.require(['engine.things.player', 'janusweb.external.JanusVOIP', 'ui.butt
       this.getAvatarData().then(avatardata => {
         // FIXME - self avatar is broken and weird right now, so it's disabled
         if (avatardata && this.room.selfavatar) {
+/*
           this.ghost = this.createObject('ghost', {
             ghost_id: this.getUsername(),
             avatar_src: 'data:text/plain,' + encodeURIComponent(avatardata),
             showlabel: false
           });
+*/
         }
       });

@@ -632,22 +654,25 @@ elation.require(['engine.things.player', 'janusweb.external.JanusVOIP', 'ui.butt
       return voipdata;
     }
     this.getAnimationID = function() {
-      var animid = 'idle';
-      if (this.controlstate.run) {
-        animid = 'run';
-      } else if (this.controlstate.move_forward) {
-        animid = 'walk';
-      } else if (this.controlstate.move_left) {
-        animid = 'walk_left';
+      var animid = this.defaultanimation;
+      let running = this.controlstate.run;
+
+      if (this.controlstate.move_left) {
+        animid = (running ? 'run' : 'walk_left');
       } else if (this.controlstate.move_right) {
-        animid = 'walk_right';
+        animid = (running ? 'run' : 'walk_right');
+      } else if (this.controlstate.move_forward) {
+        animid = (running ? 'run' : 'walk');
       } else if (this.controlstate.move_backward) {
-        animid = 'walk_back';
+        animid = (running ? 'run' : 'walk_back');
       } else if (document.activeElement && this.properties.janus.chat && document.activeElement === this.properties.janus.chat.input.inputelement) {
         animid = 'type';
       } else if (this.hasVoipData()) {
         animid = 'speak';
       }
+      if (this.ghost && this.ghost.body) {
+        this.ghost.body.anim_id = animid;
+      }
       return animid;
     }
     this.setHand = function(handedness, handobj) {
@@ -795,7 +820,8 @@ elation.require(['engine.things.player', 'janusweb.external.JanusVOIP', 'ui.butt
         if (this.collision_radius > 0) {
           this.setCollider('sphere', {
             radius: this.collision_radius,
-            offset: V(0, this.collision_radius, 0)
+            length: this.height,
+            //offset: V(0, this.collision_radius, 0)
           });
         } else {
           this.removeCollider();
@@ -1036,6 +1062,21 @@ elation.require(['engine.things.player', 'janusweb.external.JanusVOIP', 'ui.butt
         round: true,
         shader_id: 'defaultportal',
       });
+
+      // FIXME - should only set this if we have an active portal animation, and we should use the animation's duration for our timeout
+      this.defaultanimation = 'portal';
+      setTimeout(() => this.defaultanimation = 'idle', 3000);
+    }
+    this.toggleCamera = function(ev) {
+      if (ev.value == 1) {
+        if (this.cameraview == 'firstperson') {
+          this.cameraview = 'thirdperson';
+          this.camera.position.z = 2;
+        } else {
+          this.cameraview = 'firstperson';
+          this.camera.position.z = 0;
+        }
+      }
     }
   }, elation.engine.things.player);
 });

-----END OF PAGE-----