diff --git a/src/assets/avatars/Bot_SingleSkin_Rigged.glb b/src/assets/avatars/Bot_SingleSkin_Rigged.glb new file mode 100755 index 0000000000000000000000000000000000000000..468db3fd21422c94a19c67fc3fd5e0d4e7b8e447 Binary files /dev/null and b/src/assets/avatars/Bot_SingleSkin_Rigged.glb differ diff --git a/src/components/animated-robot-hands.js b/src/components/animated-robot-hands.js new file mode 100644 index 0000000000000000000000000000000000000000..ef7b429d177864cc4d78a907f333e5e38411d7f9 --- /dev/null +++ b/src/components/animated-robot-hands.js @@ -0,0 +1,97 @@ +// 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", { + schema: { + leftHand: { type: "selector", default: "#player-left-controller" }, + rightHand: { type: "selector", default: "#player-right-controller" } + }, + + init: function() { + window.hands = this; + this.playAnimation = this.playAnimation.bind(this); + + // Get the three.js object in the scene graph that has the animation data + const root = this.el.querySelector("a-gltf-entity .RootScene").object3D.children[0]; + this.mixer = new THREE.AnimationMixer(root); + this.root = root; + + // Set hands to open pose because the bind pose is funky due + // to the workaround for FBX2glTF animations. + this.openL = this.mixer.clipAction(POSES.open + "_L", root.parent); + this.openR = this.mixer.clipAction(POSES.open + "_R", root.parent); + 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); + }, + + tick: function(t, dt) { + this.mixer.update(dt / 1000); + }, + + // 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.root.parent); + const to = mixer.clipAction(currPose, this.root.parent); + 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-controls2.js b/src/components/hand-controls2.js index a466ad7ba80b9591dd45eb18442e41466e3fb411..af86cd5da272ea5aee5c60e75578da84a13aaef5 100644 --- a/src/components/hand-controls2.js +++ b/src/components/hand-controls2.js @@ -1,15 +1,13 @@ -const GESTURES = { +const POSES = { open: "open", - // point: grip active, trackpad surface active, trigger inactive. point: "point", - // pointThumb: grip active, trigger inactive, trackpad surface inactive. - pointThumb: "pointThumb", - // fist: grip active, trigger active, trackpad surface active. fist: "fist", - // hold: trigger active, grip inactive. hold: "hold", - // thumbUp: grip active, trigger active, trackpad surface inactive. - thumbUp: "thumbUp" + thumbUp: "thumbUp", + thumbDown: "thumbDown", + indexDown: "indexDown", + pinch: "pinch", + mrpDown: "mrpDown" }; const CONTROLLER_OFFSETS = { @@ -30,7 +28,7 @@ AFRAME.registerComponent("hand-controls2", { init() { const el = this.el; - this.gesture = GESTURES.open; + this.pose = POSES.open; this.fingersDown = { thumb: false, @@ -40,31 +38,31 @@ AFRAME.registerComponent("hand-controls2", { pinky: false }; - this.onMiddleRingPinkyDown = this.updateGesture.bind(this, { + this.onMiddleRingPinkyDown = this.updatePose.bind(this, { middle: true, ring: true, pinky: true }); - this.onMiddleRingPinkyUp = this.updateGesture.bind(this, { + this.onMiddleRingPinkyUp = this.updatePose.bind(this, { middle: false, ring: false, pinky: false }); - this.onIndexDown = this.updateGesture.bind(this, { + this.onIndexDown = this.updatePose.bind(this, { index: true }); - this.onIndexUp = this.updateGesture.bind(this, { + this.onIndexUp = this.updatePose.bind(this, { index: false }); - this.onThumbDown = this.updateGesture.bind(this, { + this.onThumbDown = this.updatePose.bind(this, { thumb: true }); - this.onThumbUp = this.updateGesture.bind(this, { + this.onThumbUp = this.updatePose.bind(this, { thumb: false }); @@ -125,41 +123,41 @@ AFRAME.registerComponent("hand-controls2", { el.removeEventListener("controllerdisconnected", this.onControllerDisconnected); }, - updateGesture(nextFingersDown) { + updatePose(nextFingersDown) { Object.assign(this.fingersDown, nextFingersDown); - const gesture = this.determineGesture(); + const pose = this.determinePose(); - if (gesture !== this.gesture) { - this.gesture = gesture; - this.el.emit(this.last + "end"); - this.el.emit(this.gesture + "start"); + if (pose !== this.pose) { + const previous = this.pose; + this.pose = pose; + this.el.emit("hand-pose", { previous: previous, current: this.pose }); } }, - determineGesture() { + determinePose() { const { thumb, index, middle, ring, pinky } = this.fingersDown; if (!thumb && !index && !middle && !ring && !pinky) { - return GESTURES.open; + return POSES.open; } else if (thumb && index && middle && ring && pinky) { - return GESTURES.fist; + return POSES.fist; } else if (!thumb && index && middle && ring && pinky) { - return GESTURES.thumbUp; + return POSES.thumbUp; } else if (!thumb && !index && middle && ring && pinky) { - return GESTURES.pointThumb; + return POSES.mrpDown; } else if (!thumb && index && !middle && !ring && !pinky) { - return GESTURES.hold; + return POSES.indexDown; } else if (thumb && !index && !middle && !ring && !pinky) { - return GESTURES.hold; + return POSES.thumbDown; } else if (thumb && index && !middle && !ring && !pinky) { - return GESTURES.hold; + return POSES.pinch; } else if (thumb && !index && middle && ring && pinky) { - return GESTURES.point; + return POSES.point; } - console.warn("Did not find matching gesture for ", this.fingersDown); + console.warn("Did not find matching pose for ", this.fingersDown); - return GESTURES.open; + return POSES.open; }, // Show controller when connected diff --git a/src/elements/a-gltf-entity.js b/src/elements/a-gltf-entity.js index 7caec327629a2d4059f0254a5a0438afe712b46b..3c9c07031eecbf21069569b0f2738fe48afcea25 100644 --- a/src/elements/a-gltf-entity.js +++ b/src/elements/a-gltf-entity.js @@ -102,6 +102,19 @@ const inflateEntities = function(classPrefix, parentEl, node) { el.setObject3D(node.type.toLowerCase(), node); + // Set the name of the `THREE.Group` to match the name of the node, + // so that `THREE.PropertyBinding` will find (and later animate) + // the group. See `PropertyBinding.findNode`: + // https://github.com/mrdoob/three.js/blob/dev/src/animation/PropertyBinding.js#L211 + el.object3D.name = node.name; + if (node.animations) { + // Pass animations up to the group object so that when we can pass the group as + // the optional root in `THREE.AnimationMixer.clipAction` and use the hierarchy + // preserved under the group (but not the node). Otherwise `clipArray` will be + // `null` in `THREE.AnimationClip.findByName`. + node.parent.animations = node.animations; + } + const entityComponents = node.userData.components; if (entityComponents) { diff --git a/src/input-mappings.js b/src/input-mappings.js index e9a80d53bf3dc8b52f4b2bee26adce3f14b73bad..622ee302f0d21d896c57650db2cefa076acef0f8 100644 --- a/src/input-mappings.js +++ b/src/input-mappings.js @@ -44,13 +44,23 @@ const config = { xbuttondown: "action_mute", gripdown: "middle_ring_pinky_down", gripup: "middle_ring_pinky_up", + abuttontouchstart: "thumb_down", + abuttontouchend: "thumb_up", + bbuttontouchstart: "thumb_down", + bbuttontouchend: "thumb_up", + xbuttontouchstart: "thumb_down", + xbuttontouchend: "thumb_up", + ybuttontouchstart: "thumb_down", + ybuttontouchend: "thumb_up", + surfacetouchstart: "thumb_down", + surfacetouchend: "thumb_up", thumbsticktouchstart: "thumb_down", thumbsticktouchend: "thumb_up", - // @TODO: How do I map more than one action to triggerdown? - // triggerdown: "index_down", - // triggerup: "index_up", - triggerdown: "action_teleport_down", - triggerup: "action_teleport_up", + triggerdown: "index_down", + triggerup: "index_up", + // @TODO: Patch AFIM to allow more than one action to be mapped to triggerdown + //triggerdown: "action_teleport_down", + //triggerup: "action_teleport_up", "axismove.reverseY": { left: "move" }, right_dpad_east: "snap_rotate_right", right_dpad_west: "snap_rotate_left", diff --git a/src/room.html b/src/room.html index 7ace5d827d993a9b518361c0b7b0b59ae6b9299f..bf25b9b22a87d6c4702546bba5abac4e179f2709 100644 --- a/src/room.html +++ b/src/room.html @@ -100,6 +100,7 @@ wasd-to-analog2d character-controller="pivot: #player-camera" ik-root + animated-robot-hands > <a-entity id="player-camera" diff --git a/src/room.js b/src/room.js index bd0ebb4577e1a6520060a5aeb363d380ccd78ea3..5af49f9b95507ad437d018af44d85251b425ed34 100644 --- a/src/room.js +++ b/src/room.js @@ -38,6 +38,7 @@ import "./components/water"; import "./components/skybox"; import "./components/layers"; import "./components/spawn-controller"; +import "./components/animated-robot-hands"; import "./components/hide-when-quality"; import "./systems/personal-space-bubble";