Project

General

Profile

Bug #125 » avatar.js

, 06/09/2022 01:29 PM

 
import { TextWriter } from './text-writer.js';
import {VRSPACEUI} from './vrspace-ui.js';

/**
GLTF 3D Avatar.
Once GLTF file is loaded, skeleton is inspected for existing arms, legs and head that can be animated.
Animation groups are also inspected and optionally modified.
Optional fixes can be applied to an avatar, typically position of an avatar, or changing the animation.
*/
export class Avatar {
/**
@param scene
@param folder ServerFolder with the content
@param shadowGenerator optional to cast shadows
*/
constructor(scene, folder, shadowGenerator) {
// parameters
this.scene = scene;
/** ServerFolder with content path */
this.folder = folder;
/** File name, default scene.gltf */
this.file = folder.file?folder.file:"scene.gltf";
/** Optional ShadowGenerator */
this.shadowGenerator = shadowGenerator;
/** Mirror mode, default true. (Switch left/right side) */
this.mirror = true;
/** Animation frames per second, default 10 */
this.fps = 10;
/** Name of the avatar/user */
this.name = 'test';
/** Height of the user, default 1.8 */
this.userHeight = 1.8;
/** Height of the ground, default 0 */
this.groundHeight = 0;
/** Object containing fixes */
this.fixes = null;
/** Wheter to generate animations for arm movement, default true */
this.animateArms = true;
/** Return to rest after cloning, default true (otherwise keeps the pose)*/
this.returnToRest = true;
/** GLTF characters are facing the user when loaded, turn it around, default false*/
this.turnAround = false;
/** Object containing author, license, source, title */
this.info = null;
// state variables
/** Once the avatar is loaded an processed, body contains body parts, e.g. body.leftArm, body.rightLeg, body.neck */
this.body = {};
/** Contains the skeleton once the avatar is loaded and processed */
this.skeleton = null;
/** Parent mesh of the avatar, used for movement and attachment */
this.parentMesh = null;
/** Original root mesh of the avatar, used to scale the avatar */
this.rootMesh = null;
this.bonesTotal = 0;
this.bonesProcessed = [];
this.bonesDepth = 0;
this.animationTargets = [];
this.character = null;
this.activeAnimation = null;
this.writer = new TextWriter(this.scene);
this.writer.billboardMode = BABYLON.Mesh.BILLBOARDMODE_ALL;
/** fetch API cache control - use no-cache in development */
this.cache = 'default';
//this.cache = 'no-cache';

/** Debug output, default false */
this.debug = false;
this.debugViewer1;
this.debugViewer2;
}

createBody() {
this.bonesTotal = 0;
this.bonesProcessed = [];
this.bonesDepth = 0;
this.neckQuat = null;
this.neckQuatInv = null;
this.headQuat = null;
this.headQuatInv = null;
this.body = {
processed: false,
root: null,
hips: null, // aka pelvis
leftLeg: {
upper: null,
lower: null,
foot: [] // foot, toe, possibly more
},
rightLeg: {
upper: null,
lower: null,
foot: []
},
spine: [], // can have one or more segments
// aka clavicle
leftArm: {
side: 'left',
frontAxis: null,
sideAxis: null,
shoulder: null,
upper: null,
upperRot: null,
lower: null,
lowerRot: null,
hand: null,
handRot: null,
fingers: {
thumb: [],
index: [],
middle: [],
ring: [],
pinky: []
}
},
rightArm: {
side: 'right',
frontAxis: null,
sideAxis: null,
shoulder: null,
upper: null,
upperRot: null,
lower: null,
lowerRot: null,
hand: null,
handRot: null,
fingers: {
thumb: [],
index: [],
middle: [],
ring: [],
pinky: []
}
},
neck: {
neck: null,
head: null,
lefEye: null,
rightEye: null
}
};
};

log( anything ) {
if ( this.debug ) {
console.log( anything );
}
}

boneProcessed(bone) {
if ( this.bonesProcessed.includes(bone.name) ) {
this.log("Already processed bone "+bone.name);
} else {
this.bonesTotal++;
this.bonesProcessed.push(bone.name);
//this.log("Processed bone "+bone.name);
}
}

/** Dispose of everything */
dispose() {
if ( this.character ) {
VRSPACEUI.assetLoader.unloadAsset(this.getUrl(), this.instantiatedEntries);
delete this.instantiatedEntries;
this.character = null;
//delete this.character.avatar;
//this.character.dispose();
}
if ( this.debugViewer1 ) {
this.debugViewer1.dispose();
}
if ( this.debugViewer2 ) {
this.debugViewer2.dispose();
}
if ( this.nameTag ) {
this.nameTag.dispose();
}
if ( this.nameMesh ) {
this.nameMesh.dispose();
this.nameParent.dispose();
}
if (this.textParent) {
this.textParent.dispose();
this.textParent = null;
}
// TODO also dispose of materials and textures (asset container)
}

/**
Utility method, dispose of avatar and return this one.
@param avatar optional avatar to dispose of
*/
replace(avatar) {
if (avatar) {
avatar.dispose();
}
return this;
}

_processContainer( container, onSuccess ) {
this.character = container;

var meshes = container.meshes;
this.rootMesh = meshes[0];
this.animationTargets = [];
if ( this.turnAround ) {
// GLTF characters are facing the user when loaded, turn it around
this.rootMesh.rotationQuaternion = this.rootMesh.rotationQuaternion.multiply(BABYLON.Quaternion.RotationAxis(BABYLON.Axis.Y,Math.PI));
}

if (container.animationGroups && container.animationGroups.length > 0) {
container.animationGroups[0].stop();
}

this.animationTargets.sort((a, b) => a.localeCompare(b, undefined, {sensitivity: 'base'}));

var bbox = this.rootMesh.getHierarchyBoundingVectors();
this.log("Bounding box:");
this.log(bbox);
var scale = this.userHeight/(bbox.max.y-bbox.min.y);
this.log("Scaling: "+scale);
this.rootMesh.scaling = new BABYLON.Vector3(scale,scale,scale);

// Adds all elements to the scene
container.addAllToScene();
this.castShadows( this.shadowGenerator );

// try to place feet on the ground
// CHECKME is this really guaranteed to work in every time?
bbox = this.rootMesh.getHierarchyBoundingVectors();
this.groundLevel(-bbox.min.y);
// CHECKME we may want to store the value in case we want to apply it again
if ( container.skeletons && container.skeletons.length > 0 ) {
// CHECKME: should we process multiple skeletons?
this.skeleton = container.skeletons[0];

this.createBody();
//this.log("bones: "+bonesTotal+" "+bonesProcessed);

this.skeleton.computeAbsoluteTransforms();
// different ways to enforce calculation:
//this.skeleton.computeAbsoluteTransforms(true);
//this.rootMesh.computeWorldMatrix(true);
//this.scene.render();
this.skeleton.name = this.folder.name;

this.processBones(this.skeleton.bones);
this.log( "Head position: "+this.headPos());
this.initialHeadPos = this.headPos();
this.resize();

//this.log(this.body);
this.bonesProcessed.sort((a, b) => a.localeCompare(b, undefined, {sensitivity: 'base'}));

this.calcLength(this.body.leftArm);
this.calcLength(this.body.rightArm);
this.calcLength(this.body.leftLeg);
this.calcLength(this.body.rightLeg);
this.guessArmsRotations();
this.guessLegsRotations();

this.body.processed = true;

if ( this.debugViewier1 || this.debugViewer2 ) {
this.scene.registerBeforeRender(() => {
if (this.debugViewer1) {
this.debugViewer1.update();
}
if (this.debugViewer2) {
this.debugViewer2.update();
}
});
}
} else {
console.log("NOT an avatar - no skeletons");
}

//this.postProcess();

this.parentMesh = container.createRootMesh();
this.parentMesh.rotationQuaternion = new BABYLON.Quaternion();
container.avatar = this;

console.log("Avatar loaded: "+this.name);
if ( onSuccess ) {
onSuccess(this);
}
}

/**
Apply fixes after loading/instantiation
*/
postProcess() {
if ( this.fixes ) {
if ( typeof this.fixes.standing !== 'undefined' ) {
// CHECKME not used since proper bounding box calculation
// might be required in some special cases
this.log( "Applying fixes for: "+this.folder.name+" standing: "+this.fixes.standing);
this.groundLevel(this.fixes.standing);
}
this.disableNodes();
if ( typeof this.fixes.autoPlay !== 'undefined' ) {
// start playing the animation
this.startAnimation(this.fixes.autoPlay);
}
}
}
/**
Load fixes from json file in the same folder, with the same name, and suffix .fixes.
Called from load().
*/
async loadFixes() {
this.log('Loading fixes from '+this.folder.relatedUrl());
if ( this.folder.related ) {
return fetch(this.folder.relatedUrl(), {cache: this.cache})
.then(response => {
if ( response.ok ) {
response.json().then(json => {
this.fixes = json;
this.log( "Loaded fixes: " );
this.log( json );
});
} else {
console.log('Error loading fixes: ' + response.status);
}
});
}
}

/**
Disable nodes marked in fixes file
*/
disableNodes() {
if ( typeof this.fixes.nodesDisabled !== 'undefined' ) {
this.enableNodes( this.fixes.nodesDisabled, false );
}
}
/**
Enable/disable given nodes
@param nodeIds array of node identifiers
@param enable true/false
*/
enableNodes( nodeIds, enable ) {
this.character.getNodes().forEach( node => {
if ( nodeIds.includes(node.id)) {
this.log("Node "+node.id+" enabled: "+enable);
node.setEnabled(enable);
}
});
}
/**
Slice an animation group
@param group AnimationGroup to slice
@param start starting key
@param end ending key
@returns new AnimationGroup containing slice of original animations
*/
sliceGroup( group, start, end ) {
var newGroup = new BABYLON.AnimationGroup(group.name+":"+start+"-"+end);
for ( var i = 0; i < group.targetedAnimations.length; i++ ) {
var slice = this.sliceAnimation( group.targetedAnimations[i].animation, start, end );
if ( slice.getKeys().length > 0 ) {
newGroup.addTargetedAnimation( slice, group.targetedAnimations[i].target );
}
}
return newGroup;
}

/**
Slice an animation
@param animation Animation to slice
@param start starting key
@param end ending key
@returns new Animation containing slice of original animation
*/
sliceAnimation(animation, start, end) {
var keys = animation.getKeys();
var slice = [];
for ( var i = 0; i < keys.length; i++ ) {
var key = keys[i];
if ( key.frame >= start ) {
if ( key.frame <= end ) {
slice.push(key);
} else {
break;
}
}
}
var ret = new BABYLON.Animation(animation.name, animation.targetProperty, animation.framePerSecond, animation.dataType, animation.enableBlending);
ret.loopMode = animation.loopMode;
ret.setKeys( slice );
return ret;
}

/**
Returns all animation groups of this avatar.
Applies fixes first, if any.
*/
getAnimationGroups(animationGroups = this.character.animationGroups) {
if (!this.animationGroups) {
var loopAnimations = true;
if ( this.fixes && typeof this.fixes.loopAnimations !== 'undefined' ) {
loopAnimations = this.fixes.loopAnimations;
}
if ( this.fixes && this.fixes.animationGroups ) {
this.animationGroups = [];
// animation groups overriden; process animation groups and generate new ones
for ( var j = 0; j < this.fixes.animationGroups.length; j++ ) {
var override = this.fixes.animationGroups[j];
// find source group
for ( var i = 0; i < animationGroups.length; i++ ) {
var group = animationGroups[i];
if ( group.name == override.source ) {
var newGroup = group;
if ( override.start || override.end ) {
// now slice it and generate new group
newGroup = this.sliceGroup( group, override.start, override.end );
}
if ( override.name ) {
newGroup.name = override.name;
}
if ( typeof override.loop !== 'undefined' ) {
newGroup.loopAnimation = override.loop;
} else {
newGroup.loopAnimation = loopAnimations;
}
this.animationGroups.push( newGroup );
break;
}
}
}
} else {
this.animationGroups = animationGroups;
for ( var i=0; i<this.animationGroups.length; i++ ) {
this.animationGroups[i].loopAnimation = loopAnimations;
}
}
}
return this.animationGroups;
}

/** Returns file name of this avatar, consisting of folder name and scene file name */
getUrl() {
return this.folder.url()+"/"+this.file;
}
/**
Loads the avatar.
@param success callback to execute on success
@param failure executed if loading fails
*/
load(success, failure) {
this.loadFixes().then( () => {
this.log("loading from "+this.folder.url());
var plugin = VRSPACEUI.assetLoader.loadAsset(
this.getUrl(),
// onSuccess:
(loadedUrl, container, info, instantiatedEntries ) => {
this.info = info
// https://doc.babylonjs.com/typedoc/classes/babylon.assetcontainer
// https://doc.babylonjs.com/typedoc/classes/babylon.instantiatedentries
if ( instantiatedEntries ) {
console.log("CHECKME: avatar "+this.name+" already loaded", container.avatar);
// copy body bones from processed avatar
this.character = container;
this.neckQuat = container.avatar.neckQuat;
this.neckQuatInv = container.avatar.neckQuatInv;
this.headQuat = container.avatar.headQuat;
this.headQuatInv = container.avatar.headQuatInv;
this.body = container.avatar.body;
// use skeleton and animationGroups from the instance
this.parentMesh = instantiatedEntries.rootNodes[0];
this.rootMesh = this.parentMesh.getChildren()[0];
if ( this.parentMesh.getChildren().length > 1 ) {
// clean up any existing text cloned along with container
console.log("Disposing of text ", this.parentMesh.getChildren()[1])
this.parentMesh.getChildren()[1].dispose();
}
this.getAnimationGroups(instantiatedEntries.animationGroups);
this.skeleton = instantiatedEntries.skeletons[0];
if ( this.returnToRest ) {
this.standUp();
this.skeleton.returnToRest();
}
this.parentMesh.rotationQuaternion = new BABYLON.Quaternion();
this.instantiatedEntries = instantiatedEntries;
if ( success ) {
success(this);
}
} else {
container.addAllToScene();
this._processContainer(container,success)
}
this.postProcess();
},
(exception)=>{
if ( failure ) {
failure(exception);
} else {
console.log(exception);
}
}
);
});
}

/** Returns position of the the head 'bone' */
headPos() {
var head = this.skeleton.bones[this.body.head];
//head.computeAbsoluteTransforms();
//head.getTransformNode().computeWorldMatrix(true);
//this.scene.render(); // FIXME workaround
console.log("Head at "+head.getAbsolutePosition()+" tran "+head.getTransformNode().getAbsolutePosition()+" root "+this.rootMesh.getAbsolutePosition(), head);
//var headPos = head.getAbsolutePosition().scale(this.rootMesh.scaling.x).add(this.rootMesh.position);
//var headPos = head.getTransformNode().getAbsolutePosition().subtract(this.rootMesh.getAbsolutePosition());
var headPos = head.getTransformNode().getAbsolutePosition();
return headPos;
}

/** Returns current height - distance head to feet */
height() {
return this.headPos().y - this.rootMesh.getAbsolutePosition().y;
}
/**
Returns absolute value of vector, i.e. Math.abs() of every value
@param vec Vector3 to get absolute
*/
absVector(vec) {
var ret = new BABYLON.Vector3();
ret.x = Math.abs(vec.x);
ret.y = Math.abs(vec.y);
ret.z = Math.abs(vec.z);
return ret;
}

/**
Returns rounded value of vector, i.e. Math.round() of every value
@param vec Vector3 to round
*/
roundVector(vec) {
vec.x = Math.round(vec.x);
vec.y = Math.round(vec.y);
vec.z = Math.round(vec.z);
}

/**
Look at given target. Head position is calculated without any bone limits.
@param t target Vector3
*/
lookAt( t ) {
var head = this.skeleton.bones[this.body.head];

// calc target pos in coordinate system of head
var totalPos = this.parentMesh.position.add(this.rootMesh.position);
var totalRot = this.rootMesh.rotationQuaternion.multiply(this.parentMesh.rotationQuaternion);
var target = new BABYLON.Vector3( t.x, t.y, t.z ).subtract(totalPos);

target.rotateByQuaternionToRef(BABYLON.Quaternion.Inverse(totalRot),target);

// CHECKME: exact calculus?
//var targetVector = target.subtract(this.headPos()).add(totalPos);
var targetVector = target.subtract(this.headPos());
if ( this.headAxisFix == -1 ) {
// FIX: neck and head opposite orientation
// businessman, robot, adventurer, unreal male
targetVector.y = -targetVector.y;
}
targetVector.rotateByQuaternionToRef(this.headQuatInv,targetVector);
// this results in weird head positions, more natural-looking fix applied after
//targetVector.rotateByQuaternionToRef(this.headQuat.multiply(this.neckQuatInv),targetVector);

var rotationMatrix = new BABYLON.Matrix();

BABYLON.Matrix.RotationAlignToRef(this.headTarget, targetVector.normalizeToNew(), rotationMatrix);
var quat = BABYLON.Quaternion.FromRotationMatrix(rotationMatrix);

if ( this.headAxisFix != 1 ) {
// FIX: neck and head opposite or under angle
// boris, businessman, robot, adventurer, unreal male
var fix = this.headQuat.multiply(this.neckQuatInv);
quat = quat.multiply(fix);
}

head.getTransformNode().rotationQuaternion = quat;
}

thirdAxis( limb ) {
var ret = new BABYLON.Vector3(1,1,1);
return ret.subtract(limb.frontAxis.axis).subtract(limb.sideAxis.axis);
}

/** Debugging helper, draws a vector between given points */
drawVector(from, to) {
BABYLON.MeshBuilder.CreateLines("vector-"+from+"-"+to, {points:[from,to]}, this.scene);
}

/**
Move given arm towards given target. Uses simplified 2-joint IK.
@param arm arm to move
@param t target position
*/
reachFor( arm, t ) {

var upperArm = this.skeleton.bones[arm.upper];
var lowerArm = this.skeleton.bones[arm.lower];

var scaling = this.rootMesh.scaling.x;

var totalPos = this.parentMesh.position.add(this.rootMesh.position);
var totalRot = this.parentMesh.rotationQuaternion.multiply(this.rootMesh.rotationQuaternion);
// current values
var armPos = upperArm.getAbsolutePosition().scale(scaling).subtract(totalPos);
var elbowPos = lowerArm.getAbsolutePosition().scale(scaling).subtract(totalPos);

var rootQuatInv = BABYLON.Quaternion.Inverse(totalRot);

// set or get initial values
if ( arm.upperQuat ) {
var upperQuat = arm.upperQuat;
var armVector = arm.armVector;
var worldQuat = arm.worldQuat;
var worldQuatInv = arm.worldQuatInv;
} else {
var worldQuat = BABYLON.Quaternion.FromRotationMatrix(upperArm.getWorldMatrix().getRotationMatrix());
arm.worldQuat = worldQuat;
this.log("Arm angles: "+worldQuat.toEulerAngles());
var worldQuatInv = BABYLON.Quaternion.Inverse(worldQuat);
arm.worldQuatInv = worldQuatInv;
var upperQuat = upperArm.getRotationQuaternion();
arm.upperQuat = upperQuat;
var armVector = elbowPos.subtract(armPos);
armVector.rotateByQuaternionToRef(worldQuatInv,armVector);
arm.armVector = armVector;
}

// calc target pos in coordinate system of character
var target = new BABYLON.Vector3(t.x, t.y, t.z).subtract(totalPos);
// CHECKME: probable bug, possibly related to worldQuat
target.rotateByQuaternionToRef(rootQuatInv,target);

// calc target vectors in local coordinate system of the arm
var targetVector = target.subtract(armPos).subtract(totalPos);
targetVector.rotateByQuaternionToRef(worldQuatInv,targetVector);

if ( arm.pointerQuat ) {

// vector pointing down in local space:
var downVector = new BABYLON.Vector3(0,-1,0);
var downRotation = new BABYLON.Matrix();
BABYLON.Matrix.RotationAlignToRef(armVector.normalizeToNew(), downVector.normalizeToNew(), downRotation);
var downQuat = BABYLON.Quaternion.FromRotationMatrix(downRotation)
// (near) parallel vectors still causing trouble
if ( isNaN(downQuat.x) || isNaN(!downQuat.y) || isNaN(!downQuat.z) || isNaN(!downQuat.y) ) {
this.log("arm vector: "+armVector+"down vector: "+downVector+" quat: "+downQuat+" rot: ");
this.log(downRotation);
// TODO: front axis, sign
downQuat = BABYLON.Quaternion.FromEulerAngles(0,0,Math.PI);
}
armVector.rotateByQuaternionToRef(downQuat,downVector);
//this.drawVector(armPos, armPos.add(downVector));

// pointer vector in mesh space:
var pointerQuat = arm.pointerQuat.multiply(rootQuatInv);
if ( this.mirror ) {
// heuristics 1, mirrored arm rotation, works well below shoulder
pointerQuat.y = - pointerQuat.y;
// heuristics 2, never point backwards
//pointerQuat.z = - pointerQuat.z;
if ( pointerQuat.z < 0 ) {
pointerQuat.z = 0;
}
} else {
// funny though this seems to just work
}

var pointerVector = new BABYLON.Vector3();
downVector.rotateByQuaternionToRef(pointerQuat,pointerVector);
//this.drawVector(armPos, armPos.add(pointerVector));
// converted to local arm space:
var sideVector = new BABYLON.Vector3();
pointerVector.rotateByQuaternionToRef(worldQuatInv,sideVector);

// rotation from current to side
var sideRotation = new BABYLON.Matrix();
BABYLON.Matrix.RotationAlignToRef(armVector.normalizeToNew(), sideVector.normalizeToNew(), sideRotation);
// rotation from side to target
var targetRotation = new BABYLON.Matrix();
BABYLON.Matrix.RotationAlignToRef(sideVector.normalizeToNew(), targetVector.normalizeToNew(), targetRotation);
var finalRotation = sideRotation.multiply(targetRotation);
} else {
// just point arm to target
var finalRotation = new BABYLON.Matrix();
BABYLON.Matrix.RotationAlignToRef(armVector.normalizeToNew(), targetVector.normalizeToNew(), finalRotation);
}

var quat = BABYLON.Quaternion.FromRotationMatrix(finalRotation);

arm.upperRot = upperQuat.multiply(quat);

// then bend arm
var length = targetVector.length();
var bent = this.bendArm(arm, length);

this.renderArmRotation(arm);
return quat;
}

// move an arms, optionally creates/updates arm animation
renderArmRotation( arm ) {
var upperArm = this.skeleton.bones[arm.upper];
var lowerArm = this.skeleton.bones[arm.lower];
if ( ! this.animateArms ) {
upperArm.getTransformNode().rotationQuaternion = arm.upperRot;
lowerArm.getTransformNode().rotationQuaternion = arm.lowerRot;
return;
}
if ( !arm.animation ) {
var armName = this.folder.name+'-'+arm.side;
var group = new BABYLON.AnimationGroup(armName+'ArmAnimation');
var upper = this._createArmAnimation(armName+"-upper");
var lower = this._createArmAnimation(armName+"-lower");
group.addTargetedAnimation(upper, this.skeleton.bones[arm.upper].getTransformNode());
group.addTargetedAnimation(lower, this.skeleton.bones[arm.lower].getTransformNode());
arm.animation = group;
}
this._updateArmAnimation(upperArm, arm.animation.targetedAnimations[0], arm.upperRot);
this._updateArmAnimation(lowerArm, arm.animation.targetedAnimations[1], arm.lowerRot);
if ( arm.animation.isPlaying ) {
arm.animation.stop();
}
arm.animation.play(false);
}
_createArmAnimation(name) {
var anim = new BABYLON.Animation(name, 'rotationQuaternion', this.fps, BABYLON.Animation.ANIMATIONTYPE_QUATERNION, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
var keys = [];
keys.push({frame:0, value: 0});
keys.push({frame:1, value: 0});
anim.setKeys(keys);
return anim;
}
_updateArmAnimation(arm, anim, dest) {
anim.animation.getKeys()[0].value = arm.getTransformNode().rotationQuaternion;
anim.animation.getKeys()[1].value = dest;
}

/**
Bend/stretch arm to a length
@param arm
@param length
*/
bendArm( arm, length ) {
var ret = true;
var upperArm = this.skeleton.bones[arm.upper];
var lowerArm = this.skeleton.bones[arm.lower];
var scaling = this.rootMesh.scaling.x;

if ( length > arm.lowerLength + arm.upperLength ) {
length = arm.lowerLength + arm.upperLength
ret = false;
}

// simplified math by using same length for both bones
// it's right angle, hypotenuse is bone
// length/2 is sinus of half of elbow angle
var boneLength = (arm.lowerLength + arm.upperLength)/2;
var innerAngle = Math.asin(length/2/boneLength);
//this.log("Bone length: "+boneLength+" distance to target "+length);
var shoulderAngle = Math.PI/2-innerAngle;
var elbowAngle = shoulderAngle*2;

var fix = BABYLON.Quaternion.RotationAxis(arm.frontAxis.axis,-shoulderAngle*arm.frontAxis.sign);
arm.upperRot = arm.upperRot.multiply(fix);

arm.lowerRot = BABYLON.Quaternion.RotationAxis(arm.frontAxis.axis,elbowAngle*arm.frontAxis.sign);
//this.log("Angle shoulder: "+shoulderAngle+" elbow: "+elbowAngle+" length: "+length);
return ret;
}

legLength() {
return (this.body.leftLeg.upperLength + this.body.leftLeg.lowerLength + this.body.rightLeg.upperLength + this.body.rightLeg.lowerLength)/2;
}

/**
Set avatar position.
@param pos postion
*/
setPosition( pos ) {
this.parentMesh.position.x = pos.x;
//this.groundLevel( pos.y ); // CHECKME
this.parentMesh.position.y = pos.y;
this.parentMesh.position.z = pos.z;
}

/**
Set avatar rotation
@param quat Quaternion
*/
setRotation( quat ) {
// FIXME this should rotate parentMesh instead
// but GLTF characters are facing the user when loaded
this.parentMesh.rotationQuaternion = quat;
}

/**
Sets the ground level
@param y height of the ground at current position
*/
groundLevel( y ) {
this.groundHeight = y;
this.rootMesh.position.y = this.rootMesh.position.y + y;
}

/**
Track user height: character may crouch or raise, or jump,
depending on heights of avatar and user.
@param height current user height
*/
trackHeight(height) {
if ( this.maxUserHeight && height != this.prevUserHeight ) {
var delta = height-this.prevUserHeight;
//this.trackDelay = 1000/this.fps;
//var speed = delta/this.trackDelay*1000; // speed in m/s
var speed = delta*this.fps;
if ( this.jumping ) {
var delay = Date.now() - this.jumping;
if ( height <= this.maxUserHeight && delay > 300 ) {
this.standUp();
this.jumping = null;
this.log("jump stopped")
} else if ( delay > 500 ) {
this.log("jump stopped - timeout")
this.standUp();
this.jumping = null;
} else {
this.jump(height - this.maxUserHeight);
}
} else if ( height > this.maxUserHeight && Math.abs(speed) > 1 ) {
// CHECKME speed is not really important here
this.jump(height - this.maxUserHeight);
this.jumping = Date.now();
this.log("jump starting")
} else {
// ignoring anything less than 1mm
if ( delta > 0.001 ) {
this.rise(delta);
} else if ( delta < -0.001 ) {
this.crouch(-delta);
}
}

} else {
this.maxUserHeight = height;
}
this.prevUserHeight = height;
}

/**
Moves the avatar to given height above the ground
@param height jump how high
*/
jump( height ) {
this.rootMesh.position.y = this.groundHeight + height;
this.changed();
}

/**
Stand up straight, at the ground, legs fully stretched
*/
standUp() {
this.jump(0);
this.bendLeg( this.body.leftLeg, 10 );
this.bendLeg( this.body.rightLeg, 10 );
}

/**
Rise a bit
@param height rise how much
*/
rise( height ) {
if ( height < 0.001 ) {
// ignoring anything less than 1mm
return;
}

if ( this.headPos().y + height > this.initialHeadPos.y ) {
height = this.initialHeadPos.y - this.headPos().y;
}
var legLength = (this.body.leftLeg.length + this.body.rightLeg.length)/2;
var length = legLength+height;
this.bendLeg( this.body.leftLeg, length );
this.bendLeg( this.body.rightLeg, length );

this.rootMesh.position.y += height;
this.changed();
}

/**
Crouch a bit
@param height how much
*/
crouch( height ) {
if ( height < 0.001 ) {
// ignoring anything less than 1mm
return;
}

var legLength = (this.body.leftLeg.length + this.body.rightLeg.length)/2;
if ( legLength - height < 0.1 ) {
height = legLength - 0.1;
}
var length = legLength-height;

this.bendLeg( this.body.leftLeg, length );
this.bendLeg( this.body.rightLeg, length );

this.rootMesh.position.y -= height;
this.changed();
}

/**
Bend/stretch leg to a length
@param leg
@param length
*/
bendLeg( leg, length ) {
if ( length < 0 ) {
console.log("ERROR: can't bend leg to "+length);
return
}
var upper = this.skeleton.bones[leg.upper];
var lower = this.skeleton.bones[leg.lower];
var scaling = this.rootMesh.scaling.x;

if ( length > leg.lowerLength + leg.upperLength ) {
length = leg.lowerLength + leg.upperLength;
if ( length == leg.length ) {
return;
}
}
leg.length = length;

if ( ! leg.upperQuat ) {
leg.upperQuat = BABYLON.Quaternion.FromRotationMatrix(upper.getWorldMatrix().getRotationMatrix());
leg.upperQuatInv = BABYLON.Quaternion.Inverse(leg.upperQuat);

leg.lowerQuat = BABYLON.Quaternion.FromRotationMatrix(lower.getWorldMatrix().getRotationMatrix());
leg.lowerQuatInv = BABYLON.Quaternion.Inverse(leg.lowerQuat);

leg.upperRot = upper.getTransformNode().rotationQuaternion.clone();
}

// simplified math by using same length for both bones
// it's right angle, hypotenuse is bone
// length/2 is sinus of half of elbow angle
var boneLength = (leg.lowerLength + leg.upperLength)/2;
var innerAngle = Math.asin(length/2/boneLength);
//this.log("Bone length: "+boneLength+" distance to target "+length);
var upperAngle = Math.PI/2-innerAngle;
var lowerAngle = upperAngle*2;

var axis = leg.frontAxis.axis;
var sign = leg.frontAxis.sign;

var upperQuat = BABYLON.Quaternion.RotationAxis(axis,upperAngle*sign);

//var upperRot = upper.getTransformNode().rotationQuaternion;
upper.getTransformNode().rotationQuaternion = leg.upperRot.multiply(upperQuat);

var fix = leg.upperQuat.multiply(leg.lowerQuatInv);
var lowerQuat = BABYLON.Quaternion.RotationAxis(axis,-lowerAngle*sign);
lowerQuat = lowerQuat.multiply(fix);

lower.getTransformNode().rotationQuaternion = lowerQuat;

return length;
}

/**
Returns lenght of an arm or leg, in absolute world coordinates.
@param limb an arm or leg
@returns total length of lower and upper arm/leg
*/
calcLength(limb) {
var upper = this.skeleton.bones[limb.upper];
var lower = this.skeleton.bones[limb.lower];
var scaling = this.rootMesh.scaling.x;
limb.upperLength = upper.getAbsolutePosition().subtract(lower.getAbsolutePosition()).length()*scaling;
if ( lower.children && lower.children[0] ) {
limb.lowerLength = lower.getAbsolutePosition().subtract(lower.children[0].getAbsolutePosition()).length()*scaling;
} else {
limb.lowerLength = 0;
}
limb.length = limb.upperLength+limb.lowerLength;
this.log("Length of "+upper.name+": "+limb.upperLength+", "+lower.name+": "+limb.lowerLength);
}

/**
Returns total weight of a vector, x+y+z
@param vector Vector3 to sum
*/
sum(vector) {
return vector.x+vector.y+vector.z;
}

// FIXME
rotateBoneTo(bone, axis, angle) {
var rotationMatrix = BABYLON.Matrix.RotationAxis(axis,angle);
var rotated = BABYLON.Quaternion.FromRotationMatrix(rotationMatrix);
bone.setRotationQuaternion(rotated);
}

rotateBoneFor(bone, axis, increment) {
var rotationMatrix = BABYLON.Matrix.RotationAxis(axis,increment);
var quat = bone.rotationQuaternion;
var rotated = BABYLON.Quaternion.FromRotationMatrix(rotationMatrix);
bone.setRotationQuaternion(quat.multiply(rotated));
}

guessArmsRotations() {

var leftUpperArm = this.skeleton.bones[this.body.leftArm.upper];
var leftLowerArm = this.skeleton.bones[this.body.leftArm.lower];
var rightUpperArm = this.skeleton.bones[this.body.rightArm.upper];
var rightLowerArm = this.skeleton.bones[this.body.rightArm.lower];

// heuristics, assume both arm rotate around same rotation axis
this.body.leftArm.sideAxis = this.guessRotation(leftUpperArm, BABYLON.Axis.Y);
//this.body.rightArm.sideAxis = this.guessRotation(rightUpperArm, BABYLON.Axis.Y);
this.body.rightArm.sideAxis = this.guessRotation(rightUpperArm, BABYLON.Axis.Y, this.body.leftArm.sideAxis.axis);

this.body.leftArm.frontAxis = this.guessRotation(leftUpperArm, BABYLON.Axis.Z);
//this.body.rightArm.frontAxis = this.guessRotation(rightUpperArm, BABYLON.Axis.Z);
this.body.rightArm.frontAxis = this.guessRotation(rightUpperArm, BABYLON.Axis.Z, this.body.leftArm.frontAxis.axis);

//this.debugViewer1 = new BABYLON.Debug.BoneAxesViewer(scene, leftUpperArm, this.rootMesh);

this.log("Left arm axis, side: "+this.body.leftArm.sideAxis.sign + this.body.leftArm.sideAxis.axis);
this.log("Left arm axis, front: "+this.body.leftArm.frontAxis.sign + this.body.leftArm.frontAxis.axis);
this.log("Right arm axis, side: "+this.body.rightArm.sideAxis.sign + this.body.rightArm.sideAxis.axis);
this.log("Right arm axis, front: "+this.body.rightArm.frontAxis.sign + this.body.rightArm.frontAxis.axis);

this.body.leftArm.upperRot = BABYLON.Quaternion.FromRotationMatrix(leftUpperArm.getRotationMatrix());
this.body.leftArm.lowerRot = BABYLON.Quaternion.FromRotationMatrix(leftLowerArm.getRotationMatrix());

this.body.rightArm.upperRot = BABYLON.Quaternion.FromRotationMatrix(rightUpperArm.getRotationMatrix());
this.body.rightArm.lowerRot = BABYLON.Quaternion.FromRotationMatrix(rightLowerArm.getRotationMatrix());
}

guessLegsRotations() {

var leftUpperLeg = this.skeleton.bones[this.body.leftLeg.upper];
var leftLowerLeg = this.skeleton.bones[this.body.leftLeg.lower];
var rightUpperLeg = this.skeleton.bones[this.body.rightLeg.upper];
var rightLowerLeg = this.skeleton.bones[this.body.rightLeg.lower];

//this.debugViewer1 = new BABYLON.Debug.BoneAxesViewer(scene, leftUpperLeg, this.rootMesh);
//this.debugViewer2 = new BABYLON.Debug.BoneAxesViewer(scene, leftLowerLeg, this.rootMesh);

this.body.leftLeg.frontAxis = this.guessRotation(leftUpperLeg, BABYLON.Axis.Z);
this.body.rightLeg.frontAxis = this.guessRotation(rightUpperLeg, BABYLON.Axis.Z, this.body.leftLeg.frontAxis.axis);

//this.log("Left leg axis, front: "+this.body.leftLeg.frontAxis.sign + this.body.leftLeg.frontAxis.axis);

this.body.leftLeg.upperRot = BABYLON.Quaternion.FromRotationMatrix(leftUpperLeg.getRotationMatrix());
this.body.leftLeg.lowerRot = BABYLON.Quaternion.FromRotationMatrix(leftLowerLeg.getRotationMatrix());

this.body.rightLeg.upperRot = BABYLON.Quaternion.FromRotationMatrix(rightUpperLeg.getRotationMatrix());
this.body.rightLeg.lowerRot = BABYLON.Quaternion.FromRotationMatrix(rightLowerLeg.getRotationMatrix());
}

guessRotation(bone, maxAxis, rotationAxis) {
var axes = [BABYLON.Axis.X,BABYLON.Axis.Y,BABYLON.Axis.Z];
if ( rotationAxis ) {
axes = [ rotationAxis ];
}
var angles = [Math.PI/2,-Math.PI/2];
var axis;
var angle;
var max = 0;
for ( var i = 0; i < axes.length; i++ ) {
for ( var j = 0; j<angles.length; j++ ) {
var ret = this.tryRotation(bone, axes[i], angles[j]).multiply(maxAxis);
var result = ret.x+ret.y+ret.z;
if ( result >= max ) {
axis = axes[i];
angle = angles[j];
max = result;
}
}
}
//this.log("Got it: "+axis+" "+angle+" - "+max);
return {axis:axis,sign:Math.sign(angle)};
}

tryRotation(bone, axis, angle) {
var target = bone.children[0];
var original = bone.getRotationQuaternion();
var oldPos = target.getAbsolutePosition();
//var oldPos = target.getTransformNode().getAbsolutePosition();
var rotationMatrix = BABYLON.Matrix.RotationAxis(axis,angle);
var quat = bone.rotationQuaternion;
var rotated = BABYLON.Quaternion.FromRotationMatrix(rotationMatrix);
bone.setRotationQuaternion(quat.multiply(rotated));
//this.scene.render(); // doesn't work in XR
//bone.computeWorldMatrix(true); // not required
bone.computeAbsoluteTransforms();
var newPos = target.getAbsolutePosition();
//var newPos = target.getTransformNode().getAbsolutePosition();
bone.setRotationQuaternion(original);
bone.computeAbsoluteTransforms();
var ret = newPos.subtract(oldPos);
this.log("Tried "+axis+" "+angle+" - "+ret.z+" "+bone.name);
return ret;
}

/**
Converts rotation quaternion of a node to euler angles
@param node
@returns Vector3 containing rotation around x,y,z
*/
euler(node) {
return node.rotationQuaternion.toEulerAngles();
}
degrees(node) {
var rot = euler(node);
return toDegrees(rot);
}

/**
Converts euler radians to degrees
@param rot Vector3 rotation around x,y,z
@returns Vector3 containing degrees around x,y,z
*/
toDegrees(rot) {
var ret = new BABYLON.Vector3();
ret.x = rot.x * 180/Math.PI;
ret.y = rot.y * 180/Math.PI;
ret.z = rot.z * 180/Math.PI;
return ret;
}

countBones(bones) {
if ( bones ) {
this.bonesDepth++;
for ( var i = 0; i < bones.length; i ++ ) {
if ( ! this.bonesProcessed.includes( bones[i].name )) {
this.boneProcessed(bones[i]);
this.processBones(bones[i].children);
}
}
}
}
processBones(bones) {
if ( bones ) {
this.bonesDepth++;
for ( var i = 0; i < bones.length; i ++ ) {
if ( ! this.bonesProcessed.includes( bones[i].name )) {
this.boneProcessed(bones[i]);
var boneName = bones[i].name.toLowerCase();
if ( ! this.body.root && boneName.includes('rootjoint') ) {
//this.body.root = bones[i].name;
this.body.root = i;
this.log("found root "+boneName+" at depth "+this.bonesDepth);
this.processBones(bones[i].children);
} else if ( ! this.body.hips && this.isHipsName(boneName) && bones[i].children.length >= 3) {
//} else if ( ! this.body.hips && bones[i].children.length >= 3) {
//this.body.hips = bones[i].name;
this.body.hips = i;
this.log("found hips "+boneName);
this.processHips(bones[i].children);
} else {
this.processBones(bones[i].children);
}
}
}
this.bonesDepth--;
}
}

isHipsName(boneName) {
return boneName.includes('pelvis') || boneName.includes('hip') || boneName.includes('spine') || boneName.includes('root');
}

processHips( bones ) {
// hips have two legs and spine attached, possibly something else
// TODO rewrite this to find most probable candidates for legs
for ( var i = 0; i < bones.length; i++ ) {
var boneName = bones[i].name.toLowerCase();
if ( boneName.includes("spine") || boneName.includes("body") ) {
this.processSpine(bones[i]);
} else if ( boneName.includes( 'left' ) || this.isLegName(boneName, 'l', bones[i].children) ) {
// left leg/thigh/upLeg/upperLeg
this.tryLeg(this.body.leftLeg, bones[i]);
} else if ( boneName.includes( 'right' ) || this.isLegName(boneName, 'r', bones[i].children)) {
// right leg/thigh/upLeg/upperLeg
this.tryLeg(this.body.rightLeg, bones[i]);
} else if ( bones[i].children.length >= 3 && this.isHipsName(boneName) ) {
this.log("Don't know how to handle bone "+boneName+", assuming hips" );
this.boneProcessed(bones[i]);
this.processHips(bones[i].children);
} else if ( bones[i].children.length > 0 ) {
this.log("Don't know how to handle bone "+boneName+", assuming spine" );
this.processSpine(bones[i]);
} else {
this.log("Don't know how to handle bone "+boneName );
this.boneProcessed(bones[i]);
}
}
}

isLegName(boneName, lr, children ) {
return boneName.includes( lr+'leg' ) ||
boneName.includes( lr+'_' ) ||
boneName.includes( ' '+lr+' ' ) ||
boneName.includes(lr+'thigh') ||
boneName.includes(lr+'hip') ||
( children.length > 0 && children[0].children.length > 0 && children[0].name.toLowerCase().includes(lr+"_") )
}

tryLeg( leg, bone ) {
if ( bone.name.toLowerCase().includes( 'thigh' ) || bone.name.toLowerCase().includes( 'leg' )) {
this.processLeg(leg, bone);
} else if (bone.children.length == 0 || bone.children.length == 1 && bone.children[0].children.length == 0) {
this.log("Ignoring bone "+bone.name);
this.boneProcessed(bone);
} else if (bone.children.length == 1 && bone.children[0].children.length == 1 && bone.children[0].children[0].children.length == 0 ) {
// children depth 2, assume leg (missing foot?)
if ( leg.upper && leg.lower ) {
this.log( "Ignoring 1-joint leg "+bone.name );
this.boneProcessed(bone);
} else {
this.log( "Processing 1-joint leg "+bone.name );
this.processLeg(leg, bone.children[0]);
}
} else {
// butt?
this.log("Don't know how to handle leg "+bone.name+", trying children");
this.boneProcessed(bone);
this.processLeg(leg, bone.children[0]);
}
}

processLeg( leg, bone ) {
this.log("Processing leg "+bone.name);
if ( leg.upper && leg.lower ) {
this.log("WARNING: leg already exists");
}
this.boneProcessed(bone);
leg.upper = this.skeleton.getBoneIndexByName(bone.name);
bone = bone.children[0];
this.boneProcessed(bone);
leg.lower = this.skeleton.getBoneIndexByName(bone.name);
if ( bone.children && bone.children[0] ) {
// foot exists
this.processFoot(leg, bone.children[0]);
}
}

processFoot( leg, bone ) {
//this.log("Processing foot "+bone.name);
this.boneProcessed(bone);
leg.foot.push(this.skeleton.getBoneIndexByName(bone.name));
if ( bone.children && bone.children.length == 1 ) {
this.processFoot( leg, bone.children[0] );
}
}

processSpine(bone) {
if ( !bone ) {
return;
}
//this.log("Processing spine "+bone.name);
// spine has at least one bone, usually 2-3,
this.boneProcessed(bone);
if ( bone.children.length == 1 ) {
this.body.spine.push(this.skeleton.getBoneIndexByName(bone.name));
this.processSpine(bone.children[0]);
} else if (bone.children.length >= 3 && this.hasHeadAndShoulders(bone) ) {
// process shoulders and neck, other joints to be ignored
for ( var i = 0; i < bone.children.length; i++ ) {
var boneName = bone.children[i].name.toLowerCase();
if ( boneName.includes( "neck" ) || boneName.includes("head") || (boneName.includes( "collar" ) && !boneName.includes( "bone" ) && !boneName.includes("lcollar") && !boneName.includes("rcollar")) ) {
if ( !boneName.includes("head") && bone.children[i].children.length > 2 ) {
this.log("Neck "+boneName+" of "+bone.name+" has "+bone.children[i].children.length+" children, assuming arms" );
this.processNeck( bone.children[i] );
this.processSpine( bone.children[i] );
} else if ( bone.name.toLowerCase().includes( "neck" ) && boneName.toLowerCase().includes("head") ) {
this.log("Arms grow out from neck?!");
this.processNeck( bone );
} else {
this.processNeck( bone.children[i] );
}
} else if (this.isArm(bone.children[i], boneName)) {
if ( boneName.includes( "left" ) || this.isArmName(boneName, 'l') ) {
this.processArms( this.body.leftArm, bone.children[i] );
} else if ( boneName.includes( "right" ) || this.isArmName(boneName, 'r') ) {
this.processArms( this.body.rightArm, bone.children[i] );
} else {
this.log("Don't know how to handle shoulder "+boneName);
this.boneProcessed(bone.children[i]);
}
} else {
this.log("Don't know how to handle bone "+boneName);
}
}
} else if ( bone.name.toLowerCase().includes("breast")) {
this.countBones(bone.children);
} else {
this.log("Not sure how to handle spine "+bone.name+", trying recursion");
this.body.spine.push(this.skeleton.getBoneIndexByName(bone.name));
this.processSpine(bone.children[0]);
}
}

isArmName(boneName, lr) {
return boneName.includes( lr+'shoulder' ) ||
boneName.includes( lr+'clavicle' ) ||
boneName.includes( lr+'collar' ) ||
boneName.includes( lr+'arm' ) ||
boneName.includes( ' '+lr+' ' ) ||
boneName.includes( lr+"_" );
}

isArm( bone, boneName ) {
//( ! boneName.includes("breast") && !boneName.includes("pistol")) {
if ( boneName.includes("shoulder") || boneName.includes("clavicle") ) {
return true;
}
return ( this.hasChildren(bone) && this.hasChildren(bone.children[0]) && this.hasChildren(bone.children[0].children[0]) )
}

hasChildren( bone ) {
return bone.children && bone.children.length > 0;
}

hasHeadAndShoulders( bone ) {
var count = 0;
for ( var i = 0; i < bone.children.length; i ++ ) {
if ( this.isHeadOrShoulder(bone.children[i]) ) {
count++;
}
}
this.log("Head and shoulders count: "+count+"/"+bone.children.length);
return count >= 3;
}

isHeadOrShoulder( bone ) {
var boneName = bone.name.toLowerCase();
return boneName.includes('head') || boneName.includes('neck') ||
((bone.children && bone.children.length > 0)
&& ( boneName.includes('shoulder')
|| boneName.includes( 'clavicle' )
|| boneName.includes( 'collar' )
|| boneName.includes( 'arm' )
));
}

processNeck( bone ) {
if ( this.body.neck.neck && this.bonesProcessed.includes(bone.name) ) {
this.log("neck "+bone.name+" already processed: "+this.body.neck.neck);
return;
}
this.log("processing neck "+bone.name+" children: "+bone.children.length);
this.body.neck = this.skeleton.getBoneIndexByName(bone.name);
this.boneProcessed(bone);
var neck = bone;
if ( bone.children && bone.children.length > 0 ) {
bone = bone.children[0];
} else {
this.log("Missing head?!");
}
this.body.head = this.skeleton.getBoneIndexByName(bone.name);
var head = bone;

var refHead = new BABYLON.Vector3();
head.getDirectionToRef(BABYLON.Axis.Z,this.rootMesh,refHead);
this.roundVector(refHead);
this.log("RefZ head: "+refHead);

var refNeck = new BABYLON.Vector3();
neck.getDirectionToRef(BABYLON.Axis.Z,this.rootMesh,refNeck);
this.roundVector(refNeck);
this.log("RefZ neck: "+refNeck);

// some characters have Z axis of neck and head pointing in opposite direction
// (rotated around Y) causing rotation to point backwards,
// they need different calculation
this.headAxisFix = refHead.z * refNeck.z;

this.headQuat = BABYLON.Quaternion.FromRotationMatrix(head.getWorldMatrix().getRotationMatrix());
this.headQuatInv = BABYLON.Quaternion.Inverse(this.headQuat);

this.neckQuat = BABYLON.Quaternion.FromRotationMatrix(neck.getWorldMatrix().getRotationMatrix());
this.neckQuatInv = BABYLON.Quaternion.Inverse(this.neckQuat);

var target = new BABYLON.Vector3(0,0,1);
target.rotateByQuaternionToRef(BABYLON.Quaternion.Inverse(this.rootMesh.rotationQuaternion),target);
target.rotateByQuaternionToRef(this.headQuatInv,target);

this.headTarget = target.negate().normalizeToNew();

this.log("Head target: "+this.headTarget+" axisFix "+this.headAxisFix);

this.boneProcessed(bone);
//this.processBones(bone.children);
this.countBones(bone.children);
}

processArms( arm, bone ) {
this.log("Processing arm "+bone.name+" "+bone.getIndex()+" "+this.skeleton.getBoneIndexByName(bone.name));
arm.shoulder = this.skeleton.getBoneIndexByName(bone.name);
this.boneProcessed(bone);
bone = bone.children[0];
arm.upper = this.skeleton.getBoneIndexByName(bone.name);
this.boneProcessed(bone);
bone = bone.children[0];
arm.lower = this.skeleton.getBoneIndexByName(bone.name);
this.boneProcessed(bone);
bone = bone.children[0];
arm.hand = this.skeleton.getBoneIndexByName(bone.name);
this.boneProcessed(bone);
if ( bone.children ) {
if ( bone.children.length == 5 ) {
for ( var i = 0; i < bone.children.length; i++ ) {
var boneName = bone.children[i].name.toLowerCase();
if ( boneName.includes("index") || boneName.includes("point") ) {
this.processFinger(arm.fingers.index, bone.children[i]);
} else if (boneName.includes("middle")) {
this.processFinger(arm.fingers.middle, bone.children[i]);
} else if (boneName.includes("pink") || boneName.includes("little")) {
this.processFinger(arm.fingers.pinky, bone.children[i]);
} else if (boneName.includes("ring")) {
this.processFinger(arm.fingers.ring, bone.children[i]);
} else if (boneName.includes("thumb")) {
this.processFinger(arm.fingers.thumb, bone.children[i]);
} else {
this.log("Can't process finger "+boneName);
this.boneProcessed(bone.children[i]);
}
}
} else {
this.log("Can't process fingers of "+bone.name+" length: "+bone.children.length);
}
}
}

processFinger( finger, bone ) {
if ( bone ) {
finger.push(this.skeleton.getBoneIndexByName(bone.name));
this.boneProcessed(bone);
if ( bone.children && bone.children.length > 0 ) {
this.processFinger(finger,bone.children[0]);
}
}
}

// unused, use only for debugging characters
processAnimations(targeted) {
var frames = [];
for ( var j = 0; j < targeted.length; j++ ) {
//this.log("animation: "+animations[j].animation.name+" target: "+animations[i].target.name);
if ( !this.animationTargets.includes(targeted[j].target.name) ) {
this.animationTargets.push(targeted[j].target.name);
if ( ! this.bonesProcessed.includes(targeted[j].target.name) ) {
this.log("Missing target "+targeted[j].target.name);
}
}
var keys = targeted[j].animation.getKeys();
for ( var i = 0; i < keys.length; i++ ) {
// square complexity
if ( ! frames.includes(keys[i].frame) ) {
frames.push( keys[i].frame );
}
}
}
}

/**
Start a given animation
@param animationName animation to start
*/
startAnimation(animationName) {
for ( var i = 0; i < this.getAnimationGroups().length; i++ ) {
var group = this.getAnimationGroups()[i];
if ( group.name == animationName ) {
//this.log("Animation group: "+animationName);
if ( group.isPlaying ) {
group.pause();
this.log("paused "+animationName);
} else {
if ( this.fixes ) {
if (typeof this.fixes.beforeAnimation !== 'undefined' ) {
this.log( "Applying fixes for: "+this.folder.name+" beforeAnimation: "+this.fixes.beforeAnimation);
this.groundLevel( this.fixes.beforeAnimation );
}
this.disableNodes();
if (typeof this.fixes.before !== 'undefined' ) {
this.fixes.before.forEach( obj => {
if ( animationName == obj.animation && obj.enableNodes ) {
console.log(obj);
this.enableNodes(obj.enableNodes, true);
}
});
}
}
this.jump(0);
group.play(group.loopAnimation);
this.log("playing "+animationName);
this.activeAnimation = animationName;
}
} else if ( group.isPlaying ) {
// stop all other animations
group.pause();
group.reset();
}
}
}

/**
Adds or remove all avatar meshes to given ShadowGenerator.
@param shadowGenerator removes shadows if null
*/
castShadows( shadowGenerator ) {
if ( this.character && this.character.meshes ) {
for ( var i = 0; i < this.character.meshes.length; i++ ) {
if (shadowGenerator) {
shadowGenerator.getShadowMap().renderList.push(this.character.meshes[i]);
} else if ( this.shadowGenerator ) {
var index = this.shadowGenerator.getShadowMap().renderList.indexOf(this.character.meshes[i]);
if ( index >= 0 ) {
this.shadowGenerator.getShadowMap().renderList.splice(index,1);
}
}
}
}
this.shadowGenerator = shadowGenerator;
}

/**
Resize the avatar taking into account userHeight and headPos.
*/
resize() {
var oldScale = this.rootMesh.scaling.y;
var oldHeadPos = this.headPos();
var scale = oldScale*this.userHeight/oldHeadPos.y;
this.rootMesh.scaling = new BABYLON.Vector3(scale,scale,scale);
//this.rootMesh.computeWorldMatrix(true);
//this.scene.render();
this.initialHeadPos = this.headPos();
this.log("Rescaling from "+oldScale+ " to "+scale+", head position from "+oldHeadPos+" to "+this.initialHeadPos);
this.changed();
return scale;
}

/**
Set the name and display it above the avatar
@param name
*/
async setName(name) {
this.writer.clear(this.parentMesh);
//this.writer.relativePosition = this.headPos().add(new BABYLON.Vector3(0,.4,0));
this.writer.relativePosition = this.rootMesh.position.add(new BABYLON.Vector3(0,.4+this.height(),0));
this.writer.write(this.parentMesh, name);
this.name = name;
}

/** Called when avatar size/height changes, TODO notify listeners */
changed() {
if ( this.nameParent ) {
var pos = this.headPos().clone();
pos.y += .4;
this.nameParent.position = pos;
}
}
async wrote(client) {
console.log(client);
var limit = 20;
var text = [this.name];
var line = '';
client.wrote.split(' ').forEach((word) => {
if ( line.length + word.length > limit ) {
text.push(line);
line = '';
}
line += word + ' ';
});
text.push(line);
console.log(text);
this.writer.clear(this.parentMesh);
//this.writer.relativePosition = this.headPos().add(new BABYLON.Vector3(0,.4+.2*(text.length-1),0));
this.writer.relativePosition = this.rootMesh.position.add(new BABYLON.Vector3(0,.4+this.height()+.2*(text.length-1),0));
this.writer.writeArray(this.parentMesh, text);
}
}
(2-2/2)