repo: janusweb action: commit revision: path_from: revision_from: b4a55df88a9b5484b51dd580c45747abf37fadfd: path_to: revision_to:
commit b4a55df88a9b5484b51dd580c45747abf37fadfd Author: James BaicoianuDate: Mon Sep 20 15:23:39 2021 -0700 Improved avatar animations and fading diff --git a/scripts/janusghost.js b/scripts/janusghost.js
--- 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
--- 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-----