diff --git a/src/components/animated-robot-hands.js b/src/components/animated-robot-hands.js deleted file mode 100644 index 1b26402848a8cb641560d76307708c4d77d53352..0000000000000000000000000000000000000000 --- a/src/components/animated-robot-hands.js +++ /dev/null @@ -1,94 +0,0 @@ -// Global THREE, AFRAME -const POSES = { - open: "allOpen", - thumbDown: "thumbDown", - indexDown: "indexDown", - mrpDown: "mrpDown", - thumbUp: "thumbsUp", - point: "point", - fist: "allGrip", - pinch: "pinch" -}; - -// TODO: When we have analog values of index-finger triggers or middle-finger grips, -// it would be nice to animate the hands proportionally to those analog values. -AFRAME.registerComponent("animated-robot-hands", { - dependencies: ["animation-mixer"], - schema: { - leftHand: { type: "selector", default: "#player-left-controller" }, - rightHand: { type: "selector", default: "#player-right-controller" } - }, - - init: function() { - this.playAnimation = this.playAnimation.bind(this); - - this.mixer = this.el.components["animation-mixer"].mixer; - - const object3DMap = this.el.object3DMap; - const rootObj = object3DMap.mesh || object3DMap.scene; - this.clipActionObject = rootObj.parent; - - // Set hands to open pose because the bind pose is funky dues - // to the workaround for FBX2glTF animations. - this.openL = this.mixer.clipAction(POSES.open + "_L", this.clipActionObject); - this.openR = this.mixer.clipAction(POSES.open + "_R", this.clipActionObject); - this.openL.play(); - this.openR.play(); - }, - - play: function() { - this.data.leftHand.addEventListener("hand-pose", this.playAnimation); - this.data.rightHand.addEventListener("hand-pose", this.playAnimation); - }, - - pause: function() { - this.data.leftHand.removeEventListener("hand-pose", this.playAnimation); - this.data.rightHand.removeEventListener("hand-pose", this.playAnimation); - }, - - // Animate from pose to pose. - // TODO: Transition from current pose (which may be BETWEEN two other poses) - // to the target pose, rather than stopping previous actions altogether. - playAnimation: function(evt) { - const isLeft = evt.target === this.data.leftHand; - // Stop the initial animations we started when the model loaded. - if (!this.openLStopped && isLeft) { - this.openL.stop(); - this.openLStopped = true; - } else if (!this.openRStopped && !isLeft) { - this.openR.stop(); - this.openRStopped = true; - } - - const { current, previous } = evt.detail; - const mixer = this.mixer; - const suffix = isLeft ? "_L" : "_R"; - const prevPose = POSES[previous] + suffix; - const currPose = POSES[current] + suffix; - - // STOP previous actions playing for this hand. - if (this["pose" + suffix + "_to"] !== undefined) { - this["pose" + suffix + "_to"].stop(); - } - if (this["pose" + suffix + "_from"] !== undefined) { - this["pose" + suffix + "_from"].stop(); - } - - const duration = 0.065; - // console.log( - // `Animating ${isLeft ? "left" : "right"} hand from ${prevPose} to ${currPose} over ${duration} seconds.` - // ); - const from = mixer.clipAction(prevPose, this.clipActionObject); - const to = mixer.clipAction(currPose, this.clipActionObject); - from.fadeOut(duration); - to.fadeIn(duration); - to.play(); - from.play(); - // Update the mixer slightly to prevent one frame of the default pose - // from appearing. TODO: Find out why that happens - this.mixer.update(0.001); - - this["pose" + suffix + "_to"] = to; - this["pose" + suffix + "_from"] = from; - } -}); diff --git a/src/components/hand-poses.js b/src/components/hand-poses.js new file mode 100644 index 0000000000000000000000000000000000000000..b5df031698e2730eeb066300acab53c5c5e1a0cb --- /dev/null +++ b/src/components/hand-poses.js @@ -0,0 +1,121 @@ +const POSES = { + open: "allOpen", + thumbDown: "thumbDown", + indexDown: "indexDown", + mrpDown: "mrpDown", + thumbUp: "thumbsUp", + point: "point", + fist: "allGrip", + pinch: "pinch" +}; + +AFRAME.registerComponent("hand-poses", { + schema: { + leftPose: { type: "string", default: "allOpen" }, + rightPose: { type: "string", default: "allOpen" }, + mixer: { type: "string" }, + gltfEntity: { type: "string", default: "a-gltf-entity" } + }, + + init() { + this.animatePose = this.animatePose.bind(this); + this.animatePoses = this.animatePoses.bind(this); + this.mixer = this.el.querySelector(this.data.mixer).components["animation-mixer"]; + + let onLoad; + onLoad = () => { + console.log("loaded!"); // TODO: This isn't getting called + this.mixer = this.el.querySelector(this.data.mixer).components["animation-mixer"]; + // this.animatePoses(); + this.el.querySelector(this.data.gltfEntity).removeEventListener("loaded", onLoad); + }; + this.el.querySelector(this.data.gltfEntity).addEventListener("loaded", onLoad); + }, + + update(oldData) { + if (!this.mixer) return; + if (!this.clipActionObject) { + const object3DMap = this.mixer.el.object3DMap; + const rootObj = object3DMap.mesh || object3DMap.scene; + this.clipActionObject = rootObj.parent; + } + if (!oldData.leftPose) { + // first update + this.leftClipFrom = this.leftClipTo = this.mixer.mixer.clipAction(POSES.open + "_L", this.clipActionObject); + this.rightClipFrom = this.rightClipTo = this.mixer.mixer.clipAction(POSES.open + "_R", this.clipActionObject); + this.leftClipTo.play(); + this.rightClipTo.play(); + } else { + this.animatePoses(oldData); + } + }, + + animatePose(hand, prev, curr) { + this[`${hand}ClipFrom`].stop(); + this[`${hand}ClipTo`].stop(); + + const duration = 0.065; + const suffix = hand == "left" ? "_L" : "_R"; + const from = (this[`${hand}ClipFrom`] = this.mixer.mixer.clipAction(prev + suffix, this.clipActionObject)); + const to = (this[`${hand}ClipTo`] = this.mixer.mixer.clipAction(curr + suffix, this.clipActionObject)); + + from.fadeOut(duration); + to.fadeIn(duration); + to.play(); + from.play(); + + this.mixer.update(0.001); + }, + + animatePoses(oldData) { + if (oldData.leftPose != this.data.leftPose) { + this.animatePose("left", oldData.leftPose, this.data.leftPose); + } + if (oldData.rightPose != this.data.rightPose) { + this.animatePose("right", oldData.rightPose, this.data.rightPose); + } + } +}); + +//TODO: Should I use the previous pose? +// Argument against: The previous pose should already be given to +// hand-poses update function, except in cases where +// a remote user has updated their poses locally at +// a faster rate than the network tick. In this case, +// I don't know whether knowing the most-recent previous +// pose is better than using the most-recently known pose. +// In either case, the resulting animation seems fine, and +// passing around only the most recent pose info saves +// bandwidth and makes the code shorter. +AFRAME.registerComponent("hand-poses-controller", { + schema: { + left: { type: "selector", default: "#player-left-controller" }, + right: { type: "selector", default: "#player-right-controller" } + }, + + init: function() { + this.setHandPose = this.setHandPose.bind(this); + + this.el.setAttribute("hand-poses", { + leftPose: POSES.open, + rightPose: POSES.open + }); + }, + + play: function() { + this.data.left.addEventListener("hand-pose", this.setHandPose); + this.data.right.addEventListener("hand-pose", this.setHandPose); + }, + + pause: function() { + this.data.left.removeEventListener("hand-pose", this.setHandPose); + this.data.right.removeEventListener("hand-pose", this.setHandPose); + }, + + setHandPose: function(evt) { + const { current, previous } = evt.detail; + const isLeft = evt.target === this.data.left; + const pose = POSES[current]; + this.el.setAttribute("hand-poses", `${isLeft ? "left" : "right"}Pose`, pose); + } +}); diff --git a/src/network-schemas.js b/src/network-schemas.js index 4632a5933807b1e97132c726fcc335afa6b68964..688ce7835c83cd67661e68472ee8f40c5b20a3a9 100644 --- a/src/network-schemas.js +++ b/src/network-schemas.js @@ -5,6 +5,14 @@ function registerNetworkSchemas() { "position", "rotation", "scale", + { + component: "hand-poses", + property: "leftPose" + }, + { + component: "hand-poses", + property: "rightPose" + }, { selector: ".camera", component: "position" diff --git a/src/room.html b/src/room.html index 52fd9ddd2a9ab2cdc4b54dba0f0093c55dee0d7f..47b7ad6654725184906b9f2dc8c78491bc9b82df 100644 --- a/src/room.html +++ b/src/room.html @@ -48,7 +48,7 @@ </template> <template id="remote-avatar-template"> - <a-entity ik-root> + <a-entity ik-root hand-poses="mixer: .RootScene"> <a-entity class="camera"></a-entity> <a-entity class="left-controller"></a-entity> @@ -57,7 +57,7 @@ <a-gltf-entity src="#bot-skinned-mesh" inflate="true" ik-controller > <template data-selector=".RootScene"> - <a-entity animation-mixer ></a-entity> + <a-entity animation-mixer></a-entity> </template> <template data-selector=".Neck"> @@ -146,6 +146,8 @@ spawn-controller="radius: 4;" wasd-to-analog2d character-controller="pivot: #player-camera" + hand-poses="mixer: .RootScene" + hand-poses-controller ik-root > <a-entity @@ -183,7 +185,7 @@ <a-gltf-entity src="#bot-skinned-mesh" inflate="true" ik-controller > <template data-selector=".RootScene"> - <a-entity animation-mixer animated-robot-hands ></a-entity> + <a-entity animation-mixer ></a-entity> </template> <template data-selector=".Neck"> diff --git a/src/room.js b/src/room.js index b09ff896a09145e54bb76ea45f9f659606eaa022..354c601f1b7ea95430ba962e4123300cadb2a38c 100644 --- a/src/room.js +++ b/src/room.js @@ -35,10 +35,10 @@ import "./components/water"; import "./components/skybox"; import "./components/layers"; import "./components/spawn-controller"; -import "./components/animated-robot-hands"; import "./components/hide-when-quality"; import "./components/animation-mixer"; import "./components/loop-animation"; +import "./components/hand-poses"; import ReactDOM from "react-dom"; import React from "react";