Skip to content
Snippets Groups Projects
ik-controller.js 7.15 KiB
Newer Older
Robert Long's avatar
Robert Long committed
const { Vector3, Quaternion, Matrix4, Euler } = THREE;
/**
 * Provides access to the end effectors for IK.
 * @namespace avatar
 * @component ik-root
 */
Robert Long's avatar
Robert Long committed
AFRAME.registerComponent("ik-root", {
  schema: {
    camera: { type: "string", default: ".camera" },
    leftController: { type: "string", default: ".left-controller" },
    rightController: { type: "string", default: ".right-controller" }
  },
  update(oldData) {
    if (this.data.camera !== oldData.camera) {
      this.camera = this.el.querySelector(this.data.camera);
    }

    if (this.data.leftController !== oldData.leftController) {
      this.leftController = this.el.querySelector(this.data.leftController);
    }

    if (this.data.rightController !== oldData.rightController) {
      this.rightController = this.el.querySelector(this.data.rightController);
    }
  }
});

function findIKRoot(entity) {
  while (entity && !(entity.components && entity.components["ik-root"])) {
    entity = entity.parentNode;
  }
  return entity && entity.components["ik-root"];
}

/**
 * Performs IK on a hip-rooted skeleton to align the hip, head and hands with camera and controller inputs.
 * @namespace avatar
 * @component ik-controller
 */
Robert Long's avatar
Robert Long committed
AFRAME.registerComponent("ik-controller", {
  schema: {
    leftEye: { type: "string", default: "LeftEye" },
    rightEye: { type: "string", default: "RightEye" },
    head: { type: "string", default: "Head" },
    neck: { type: "string", default: "Neck" },
    leftHand: { type: "string", default: "LeftHand" },
    rightHand: { type: "string", default: "RightHand" },
    chest: { type: "string", default: "Chest" },
    hips: { type: "string", default: "Hips" },
Robert Long's avatar
Robert Long committed
    rotationSpeed: { default: 5 }
  },

  init() {
    this.flipY = new Matrix4().makeRotationY(Math.PI);

    this.cameraForward = new Matrix4();
    this.headTransform = new Matrix4();
    this.hipsPosition = new Vector3();

    this.invHipsToHeadVector = new Vector3();

    this.middleEyeMatrix = new Matrix4();
    this.middleEyePosition = new Vector3();
    this.invMiddleEyeToHead = new Matrix4();

    this.cameraYRotation = new Euler();
    this.cameraYQuaternion = new Quaternion();

    this.invHipsQuaternion = new Quaternion();
    this.headQuaternion = new Quaternion();

    this.rootToChest = new Matrix4();
    this.invRootToChest = new Matrix4();

Robert Long's avatar
Robert Long committed
    this.hands = {
      left: {
        rotation: new Matrix4().makeRotationFromEuler(new Euler(-Math.PI / 2, Math.PI / 2, 0))
      },
      right: {
        rotation: new Matrix4().makeRotationFromEuler(new Euler(Math.PI / 2, Math.PI / 2, 0))
      }
    };
  },

  update(oldData) {
    if (this.data.leftEye !== oldData.leftEye) {
      this.leftEye = this.el.object3D.getObjectByName(this.data.leftEye);
Robert Long's avatar
Robert Long committed
    }

    if (this.data.rightEye !== oldData.rightEye) {
      this.rightEye = this.el.object3D.getObjectByName(this.data.rightEye);
Robert Long's avatar
Robert Long committed
    }

    if (this.data.head !== oldData.head) {
      this.head = this.el.object3D.getObjectByName(this.data.head);
Robert Long's avatar
Robert Long committed
    }

    if (this.data.neck !== oldData.neck) {
      this.neck = this.el.object3D.getObjectByName(this.data.neck);
Robert Long's avatar
Robert Long committed
    }

    if (this.data.leftHand !== oldData.leftHand) {
      this.leftHand = this.el.object3D.getObjectByName(this.data.leftHand);
Robert Long's avatar
Robert Long committed
    }

    if (this.data.rightHand !== oldData.rightHand) {
      this.rightHand = this.el.object3D.getObjectByName(this.data.rightHand);
Robert Long's avatar
Robert Long committed
    }

    if (this.data.chest !== oldData.chest) {
      this.chest = this.el.object3D.getObjectByName(this.data.chest);
Robert Long's avatar
Robert Long committed
    }

    if (this.data.hips !== oldData.hips) {
      this.hips = this.el.object3D.getObjectByName(this.data.hips);
Robert Long's avatar
Robert Long committed
    }

    // Set middleEye's position to be right in the middle of the left and right eyes.
    this.middleEyePosition.addVectors(this.leftEye.position, this.rightEye.position);
Robert Long's avatar
Robert Long committed
    this.middleEyePosition.divideScalar(2);
    this.middleEyeMatrix.makeTranslation(this.middleEyePosition.x, this.middleEyePosition.y, this.middleEyePosition.z);
    this.invMiddleEyeToHead = this.middleEyeMatrix.getInverse(this.middleEyeMatrix);

    this.invHipsToHeadVector
      .addVectors(this.chest.position, this.neck.position)
      .add(this.head.position)
Robert Long's avatar
Robert Long committed
      .negate();
  },

  tick(time, dt) {
    if (!this.ikRoot) {
      return;
    }

    const { camera, leftController, rightController } = this.ikRoot;
    const {
      hips,
      head,
      chest,
      cameraForward,
      headTransform,
      invMiddleEyeToHead,
      invHipsToHeadVector,
      flipY,
      cameraYRotation,
      cameraYQuaternion,
      invHipsQuaternion,
      leftHand,
      rightHand,
      rootToChest,
      invRootToChest
    } = this;

    // Camera faces the -Z direction. Flip it along the Y axis so that it is +Z.
    camera.object3D.updateMatrix();
    cameraForward.multiplyMatrices(camera.object3D.matrix, flipY);

    // Compute the head position such that the hmd position would be in line with the middleEye
    headTransform.multiplyMatrices(cameraForward, invMiddleEyeToHead);

    // Then position the hips such that the head is aligned with headTransform
    // (which positions middleEye in line with the hmd)
    hips.position.setFromMatrixPosition(headTransform).add(invHipsToHeadVector);
Robert Long's avatar
Robert Long committed

    // Animate the hip rotation to follow the Y rotation of the camera with some damping.
    cameraYRotation.setFromRotationMatrix(cameraForward, "YXZ");
    cameraYRotation.x = 0;
    cameraYRotation.z = 0;
    cameraYQuaternion.setFromEuler(cameraYRotation);
Marshall Quander's avatar
Marshall Quander committed
    Quaternion.slerp(hips.quaternion, cameraYQuaternion, hips.quaternion, (this.data.rotationSpeed * dt) / 1000);
    // Take the head orientation computed from the hmd, remove the Y rotation already applied to it by the hips,
    // and apply it to the head
    invHipsQuaternion.copy(hips.quaternion).inverse();
    head.quaternion.setFromRotationMatrix(headTransform).premultiply(invHipsQuaternion);
    hips.updateMatrix();
    rootToChest.multiplyMatrices(hips.matrix, chest.matrix);
Robert Long's avatar
Robert Long committed
    invRootToChest.getInverse(rootToChest);

    this.updateHand(this.hands.left, leftHand, leftController, true);
    this.updateHand(this.hands.right, rightHand, rightController, false);
  updateHand(handState, handObject3D, controller, isLeft) {
    const hand = handObject3D.el;
Robert Long's avatar
Robert Long committed
    const handMatrix = handObject3D.matrix;
    const controllerObject3D = controller.object3D;

    // TODO: This coupling with personal-space-invader is not ideal.
    // There should be some intermediate thing managing multiple opinions about object visibility
    const spaceInvader = hand.components["personal-space-invader"];
    const handHiddenByPersonalSpace = spaceInvader && spaceInvader.invading;

    handObject3D.visible = !handHiddenByPersonalSpace && controllerObject3D.visible;
Robert Long's avatar
Robert Long committed
    if (controllerObject3D.visible) {
      handMatrix.multiplyMatrices(this.invRootToChest, controllerObject3D.matrix);

      const handControls = controller.components["hand-controls2"];

      if (handControls) {
        handMatrix.multiply(isLeft ? handControls.getLeftControllerOffset() : handControls.getRightControllerOffset());
Robert Long's avatar
Robert Long committed
      }

      handMatrix.multiply(handState.rotation);

      handObject3D.position.setFromMatrixPosition(handMatrix);
      handObject3D.rotation.setFromRotationMatrix(handMatrix);
    }
  }
});