439 lines
16 KiB
C#
439 lines
16 KiB
C#
// Copyright 2016 Google Inc. All rights reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
// The controller is not available for versions of Unity without the
|
|
// GVR native integration.
|
|
|
|
using UnityEngine;
|
|
using UnityEngine.VR;
|
|
using System.Collections;
|
|
|
|
/// The GvrArmModel is a standard interface to interact with a scene with the controller.
|
|
/// It is responsible for:
|
|
/// - Determining the orientation and location of the controller.
|
|
/// - Predict the location of the shoulder, elbow, wrist, and pointer.
|
|
///
|
|
/// There should only be one instance in the scene, and it should be attached
|
|
/// to the GvrController.
|
|
[RequireComponent(typeof(GvrController))]
|
|
public class GvrArmModel : MonoBehaviour {
|
|
#if UNITY_HAS_GOOGLEVR && (UNITY_ANDROID || UNITY_EDITOR)
|
|
private static GvrArmModel instance = null;
|
|
|
|
/// Initial relative location of the shoulder (meters).
|
|
private static readonly Vector3 DEFAULT_SHOULDER_RIGHT = new Vector3(0.19f, -0.19f, -0.03f);
|
|
|
|
/// The range of movement from the elbow position due to accelerometer (meters).
|
|
private static readonly Vector3 ELBOW_MIN_RANGE = new Vector3(-0.05f, -0.1f, 0.0f);
|
|
private static readonly Vector3 ELBOW_MAX_RANGE = new Vector3(0.05f, 0.1f, 0.2f);
|
|
|
|
/// Offset of the laser pointer origin relative to the wrist (meters)
|
|
private static readonly Vector3 POINTER_OFFSET = new Vector3(0.0f, -0.009f, 0.099f);
|
|
|
|
/// Rest position parameters for arm model (meters).
|
|
private static readonly Vector3 ELBOW_POSITION = new Vector3(0.195f, -0.5f, -0.075f);
|
|
private static readonly Vector3 WRIST_POSITION = new Vector3(0.0f, 0.0f, 0.25f);
|
|
private static readonly Vector3 ARM_EXTENSION_OFFSET = new Vector3(-0.13f, 0.14f, 0.08f);
|
|
|
|
/// Strength of the acceleration filter (unitless).
|
|
private const float GRAVITY_CALIB_STRENGTH = 0.999f;
|
|
|
|
/// Strength of the velocity suppression (unitless).
|
|
private const float VELOCITY_FILTER_SUPPRESS = 0.99f;
|
|
|
|
/// Strength of the velocity suppression during low acceleration (unitless).
|
|
private const float LOW_ACCEL_VELOCITY_SUPPRESS = 0.9f;
|
|
|
|
/// Strength of the acceleration suppression during low velocity (unitless).
|
|
private const float LOW_VELOCITY_ACCEL_SUPPRESS = 0.5f;
|
|
|
|
/// The minimum allowable accelerometer reading before zeroing (m/s^2).
|
|
private const float MIN_ACCEL = 1.0f;
|
|
|
|
/// The expected force of gravity (m/s^2).
|
|
private const float GRAVITY_FORCE = 9.807f;
|
|
|
|
/// Amount of normalized alpha transparency to change per second.
|
|
private const float DELTA_ALPHA = 4.0f;
|
|
|
|
/// Angle ranges the for arm extension offset to start and end (degrees).
|
|
private const float MIN_EXTENSION_ANGLE = 7.0f;
|
|
private const float MAX_EXTENSION_ANGLE = 60.0f;
|
|
|
|
/// Increases elbow bending as the controller moves up (unitless).
|
|
private const float EXTENSION_WEIGHT = 0.4f;
|
|
|
|
/// Offset of the elbow due to the accelerometer
|
|
private Vector3 elbowOffset;
|
|
|
|
/// Forward direction of the arm model.
|
|
private Vector3 torsoDirection;
|
|
|
|
/// Filtered velocity of the controller.
|
|
private Vector3 filteredVelocity;
|
|
|
|
/// Filtered acceleration of the controller.
|
|
private Vector3 filteredAccel;
|
|
|
|
/// Used to calibrate the ambient gravitational force.
|
|
private Vector3 zeroAccel;
|
|
|
|
/// Indicates if this is the first frame to receive new IMU measurements.
|
|
private bool firstUpdate;
|
|
|
|
/// Multiplier for handedness such that 1 = Right, 0 = Center, -1 = left.
|
|
private Vector3 handedMultiplier;
|
|
|
|
#if UNITY_EDITOR
|
|
private Camera editorHeadCamera;
|
|
#endif // UNITY_EDITOR
|
|
|
|
/// Use the GvrController singleton to obtain a singleton for this class.
|
|
public static GvrArmModel Instance {
|
|
get {
|
|
if (instance == null) {
|
|
instance = GvrController.ArmModel;
|
|
}
|
|
return instance != null && instance.isActiveAndEnabled ? instance : null;
|
|
}
|
|
}
|
|
|
|
/// Represents when gaze-following behavior should occur.
|
|
public enum GazeBehavior {
|
|
Never, /// The shoulder will never follow the gaze.
|
|
DuringMotion, /// The shoulder will follow the gaze during controller motion.
|
|
Always /// The shoulder will always follow the gaze.
|
|
}
|
|
|
|
/// Height of the elbow (m).
|
|
[Range(0.0f, 0.2f)]
|
|
public float addedElbowHeight = 0.0f;
|
|
|
|
/// Depth of the elbow (m).
|
|
[Range(0.0f, 0.2f)]
|
|
public float addedElbowDepth = 0.0f;
|
|
|
|
/// The Downward tilt or pitch of the laser pointer relative to the controller (degrees).
|
|
[Range(0.0f, 30.0f)]
|
|
public float pointerTiltAngle = 15.0f;
|
|
|
|
/// Controller distance from the face after which the controller disappears (meters).
|
|
[Range(0.0f, 0.4f)]
|
|
public float fadeDistanceFromFace = 0.32f;
|
|
|
|
/// Controller distance from face after which the tooltips appear (meters).
|
|
[Range(0.4f, 0.6f)]
|
|
public float tooltipMinDistanceFromFace = 0.45f;
|
|
|
|
/// When the angle (degrees) between the controller and the head is larger than
|
|
/// this value, the tooltips disappear.
|
|
/// If the value is 180, then the tooltips are always shown.
|
|
/// If the value is 90, the tooltips are only shown when they are facing the camera.
|
|
[Range(0, 180)]
|
|
public int tooltipMaxAngleFromCamera = 80;
|
|
|
|
/// Determines if the shoulder should follow the gaze
|
|
public GazeBehavior followGaze = GazeBehavior.DuringMotion;
|
|
|
|
/// Determines if the accelerometer should be used.
|
|
public bool useAccelerometer = false;
|
|
|
|
/// Vector to represent the pointer's location.
|
|
/// NOTE: This is in meatspace coordinates.
|
|
public Vector3 pointerPosition { get; private set; }
|
|
|
|
/// Quaternion to represent the pointer's rotation.
|
|
/// NOTE: This is in meatspace coordinates.
|
|
public Quaternion pointerRotation { get; private set; }
|
|
|
|
/// Vector to represent the wrist's location.
|
|
/// NOTE: This is in meatspace coordinates.
|
|
public Vector3 wristPosition { get; private set; }
|
|
|
|
/// Quaternion to represent the wrist's rotation.
|
|
/// NOTE: This is in meatspace coordinates.
|
|
public Quaternion wristRotation { get; private set; }
|
|
|
|
/// Vector to represent the elbow's location.
|
|
/// NOTE: This is in meatspace coordinates.
|
|
public Vector3 elbowPosition { get; private set; }
|
|
|
|
/// Quaternion to represent the elbow's rotation.
|
|
/// NOTE: This is in meatspace coordinates.
|
|
public Quaternion elbowRotation { get; private set; }
|
|
|
|
/// Vector to represent the shoulder's location.
|
|
/// NOTE: This is in meatspace coordinates.
|
|
public Vector3 shoulderPosition { get; private set; }
|
|
|
|
/// Vector to represent the shoulder's location.
|
|
/// NOTE: This is in meatspace coordinates.
|
|
public Quaternion shoulderRotation { get; private set; }
|
|
|
|
/// The suggested rendering alpha value of the controller.
|
|
/// This is to prevent the controller from intersecting the face.
|
|
/// The range is always 0 - 1 but can be scaled by individual
|
|
/// objects when using the GvrBaseControllerVisual script.
|
|
public float preferredAlpha { get; private set; }
|
|
|
|
/// The suggested rendering alpha value of the controller tooltips.
|
|
/// This is to only display the tooltips when the player is looking
|
|
/// at the controller, and also to prevent the tooltips from intersecting the
|
|
/// player's face.
|
|
public float tooltipAlphaValue { get; private set; }
|
|
|
|
/// Event handler that occurs when the state of the ArmModel is updated.
|
|
public delegate void OnArmModelUpdateEvent();
|
|
public event OnArmModelUpdateEvent OnArmModelUpdate;
|
|
|
|
void Start() {
|
|
// Obtain the Gvr controller from the scene.
|
|
GvrController controller = GetComponent<GvrController>();
|
|
|
|
UpdateHandedness();
|
|
|
|
// Register the controller update listener.
|
|
controller.OnControllerUpdate += OnControllerUpdate;
|
|
|
|
// Reset other relevant state.
|
|
firstUpdate = true;
|
|
elbowOffset = Vector3.zero;
|
|
zeroAccel.Set(0, GRAVITY_FORCE, 0);
|
|
}
|
|
|
|
void OnDestroy() {
|
|
// Unregister the controller update listener.
|
|
GvrController controller = GetComponent<GvrController>();
|
|
controller.OnControllerUpdate -= OnControllerUpdate;
|
|
|
|
// Reset the singleton instance.
|
|
instance = null;
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
void Update() {
|
|
editorHeadCamera = Camera.main;
|
|
}
|
|
#endif // UNITY_EDITOR
|
|
|
|
private void OnControllerUpdate() {
|
|
if (GvrController.Recentered) {
|
|
ResetState();
|
|
}
|
|
|
|
UpdateHandedness();
|
|
UpdateTorsoDirection();
|
|
if (GvrController.State == GvrConnectionState.Connected) {
|
|
UpdateFromController();
|
|
} else {
|
|
ResetState();
|
|
}
|
|
if (useAccelerometer) {
|
|
UpdateVelocity();
|
|
TransformElbow();
|
|
} else {
|
|
elbowOffset = Vector3.zero;
|
|
}
|
|
ApplyArmModel();
|
|
UpdateTransparency();
|
|
UpdatePointer();
|
|
|
|
firstUpdate = false;
|
|
if (OnArmModelUpdate != null) {
|
|
OnArmModelUpdate();
|
|
}
|
|
}
|
|
|
|
private void UpdateHandedness() {
|
|
// Update user handedness if the setting has changed
|
|
GvrSettings.UserPrefsHandedness handedness = GvrSettings.Handedness;
|
|
|
|
// Determine handedness multiplier.
|
|
handedMultiplier.Set(0, 1, 1);
|
|
if (handedness == GvrSettings.UserPrefsHandedness.Right) {
|
|
handedMultiplier.x = 1.0f;
|
|
} else if (handedness == GvrSettings.UserPrefsHandedness.Left) {
|
|
handedMultiplier.x = -1.0f;
|
|
}
|
|
|
|
// Place the shoulder in anatomical positions based on the height and handedness.
|
|
shoulderRotation = Quaternion.identity;
|
|
shoulderPosition = Vector3.Scale(DEFAULT_SHOULDER_RIGHT, handedMultiplier);
|
|
}
|
|
|
|
private Vector3 GetHeadOrientation() {
|
|
#if UNITY_EDITOR
|
|
if (editorHeadCamera == null) {
|
|
Debug.LogWarning("No Head Camera.");
|
|
return Vector3.forward;
|
|
}
|
|
|
|
Vector3 forward = editorHeadCamera.transform.forward;
|
|
if (editorHeadCamera.transform.parent != null) {
|
|
forward = editorHeadCamera.transform.parent.InverseTransformDirection(forward);
|
|
}
|
|
|
|
return forward;
|
|
#else
|
|
return InputTracking.GetLocalRotation(VRNode.Head) * Vector3.forward;
|
|
#endif // UNITY_EDITOR
|
|
}
|
|
|
|
private void UpdateTorsoDirection() {
|
|
// Ignore updates here if requested.
|
|
if (followGaze == GazeBehavior.Never) {
|
|
return;
|
|
}
|
|
|
|
// Determine the gaze direction horizontally.
|
|
Vector3 gazeDirection = GetHeadOrientation();
|
|
gazeDirection.y = 0.0f;
|
|
gazeDirection.Normalize();
|
|
|
|
// Use the gaze direction to update the forward direction.
|
|
if (followGaze == GazeBehavior.Always || firstUpdate) {
|
|
torsoDirection = gazeDirection;
|
|
} else if (followGaze == GazeBehavior.DuringMotion) {
|
|
float angularVelocity = GvrController.Gyro.magnitude;
|
|
float gazeFilterStrength = Mathf.Clamp((angularVelocity - 0.2f) / 45.0f, 0.0f, 0.1f);
|
|
torsoDirection = Vector3.Slerp(torsoDirection, gazeDirection, gazeFilterStrength);
|
|
}
|
|
|
|
// Rotate the fixed joints.
|
|
Quaternion gazeRotation = Quaternion.FromToRotation(Vector3.forward, torsoDirection);
|
|
shoulderRotation = gazeRotation;
|
|
shoulderPosition = gazeRotation * shoulderPosition;
|
|
}
|
|
|
|
private void UpdateFromController() {
|
|
// Get the orientation-adjusted acceleration.
|
|
Vector3 accel = GvrController.Orientation * GvrController.Accel;
|
|
|
|
// Very slowly calibrate gravity force out of acceleration.
|
|
zeroAccel = zeroAccel * GRAVITY_CALIB_STRENGTH + accel * (1.0f - GRAVITY_CALIB_STRENGTH);
|
|
filteredAccel = accel - zeroAccel;
|
|
|
|
// If no tracking history, reset the velocity.
|
|
if (firstUpdate) {
|
|
filteredVelocity = Vector3.zero;
|
|
}
|
|
|
|
// IMPORTANT: The accelerometer is not reliable at these low magnitudes
|
|
// so ignore it to prevent drift.
|
|
if (filteredAccel.magnitude < MIN_ACCEL) {
|
|
// Suppress the acceleration.
|
|
filteredAccel = Vector3.zero;
|
|
filteredVelocity *= LOW_ACCEL_VELOCITY_SUPPRESS;
|
|
} else {
|
|
// If the velocity is decreasing, prevent snap-back by reducing deceleration.
|
|
Vector3 newVelocity = filteredVelocity + filteredAccel * Time.deltaTime;
|
|
if (newVelocity.sqrMagnitude < filteredVelocity.sqrMagnitude) {
|
|
filteredAccel *= LOW_VELOCITY_ACCEL_SUPPRESS;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void UpdateVelocity() {
|
|
// Update the filtered velocity.
|
|
filteredVelocity += filteredAccel * Time.deltaTime;
|
|
filteredVelocity *= VELOCITY_FILTER_SUPPRESS;
|
|
}
|
|
|
|
private void ResetState() {
|
|
// We've lost contact, quickly reset the state.
|
|
filteredVelocity *= 0.5f;
|
|
filteredAccel *= 0.5f;
|
|
firstUpdate = true;
|
|
}
|
|
|
|
private void TransformElbow() {
|
|
// Apply the filtered velocity to update the elbow offset position.
|
|
if (useAccelerometer) {
|
|
elbowOffset += filteredVelocity * Time.deltaTime;
|
|
elbowOffset.x = Mathf.Clamp(elbowOffset.x, ELBOW_MIN_RANGE.x, ELBOW_MAX_RANGE.x);
|
|
elbowOffset.y = Mathf.Clamp(elbowOffset.y, ELBOW_MIN_RANGE.y, ELBOW_MAX_RANGE.y);
|
|
elbowOffset.z = Mathf.Clamp(elbowOffset.z, ELBOW_MIN_RANGE.z, ELBOW_MAX_RANGE.z);
|
|
}
|
|
}
|
|
|
|
private void ApplyArmModel() {
|
|
// Find the controller's orientation relative to the player
|
|
Quaternion controllerOrientation = GvrController.Orientation;
|
|
controllerOrientation = Quaternion.Inverse(shoulderRotation) * controllerOrientation;
|
|
|
|
// Get the relative positions of the joints
|
|
elbowPosition = ELBOW_POSITION + new Vector3(0.0f, addedElbowHeight, addedElbowDepth);
|
|
elbowPosition = Vector3.Scale(elbowPosition, handedMultiplier) + elbowOffset;
|
|
wristPosition = Vector3.Scale(WRIST_POSITION, handedMultiplier);
|
|
Vector3 armExtensionOffset = Vector3.Scale(ARM_EXTENSION_OFFSET, handedMultiplier);
|
|
|
|
// Extract just the x rotation angle
|
|
Vector3 controllerForward = controllerOrientation * Vector3.forward;
|
|
float xAngle = 90.0f - Vector3.Angle(controllerForward, Vector3.up);
|
|
|
|
// Remove the z rotation from the controller
|
|
Quaternion xyRotation = Quaternion.FromToRotation(Vector3.forward, controllerForward);
|
|
|
|
// Offset the elbow by the extension
|
|
float normalizedAngle = (xAngle - MIN_EXTENSION_ANGLE) / (MAX_EXTENSION_ANGLE - MIN_EXTENSION_ANGLE);
|
|
float extensionRatio = Mathf.Clamp(normalizedAngle, 0.0f, 1.0f);
|
|
if (!useAccelerometer) {
|
|
elbowPosition += armExtensionOffset * extensionRatio;
|
|
}
|
|
|
|
// Calculate the lerp interpolation factor
|
|
float totalAngle = Quaternion.Angle(xyRotation, Quaternion.identity);
|
|
float lerpSuppresion = 1.0f - Mathf.Pow(totalAngle / 180.0f, 6);
|
|
float lerpValue = lerpSuppresion * (0.4f + 0.6f * extensionRatio * EXTENSION_WEIGHT);
|
|
|
|
// Apply the absolute rotations to the joints
|
|
Quaternion lerpRotation = Quaternion.Lerp(Quaternion.identity, xyRotation, lerpValue);
|
|
elbowRotation = shoulderRotation * Quaternion.Inverse(lerpRotation) * controllerOrientation;
|
|
wristRotation = shoulderRotation * controllerOrientation;
|
|
|
|
// Determine the relative positions
|
|
elbowPosition = shoulderRotation * elbowPosition;
|
|
wristPosition = elbowPosition + elbowRotation * wristPosition;
|
|
}
|
|
|
|
private void UpdateTransparency() {
|
|
// Determine how vertical the controller is pointing.
|
|
float animationDelta = DELTA_ALPHA * Time.deltaTime;
|
|
float distToFace = Vector3.Distance(wristPosition, Vector3.zero);
|
|
if (distToFace < fadeDistanceFromFace) {
|
|
preferredAlpha = Mathf.Max(0.0f, preferredAlpha - animationDelta);
|
|
} else {
|
|
preferredAlpha = Mathf.Min(1.0f, preferredAlpha + animationDelta);
|
|
}
|
|
|
|
float dot = Vector3.Dot(wristRotation * Vector3.up, -wristPosition.normalized);
|
|
float minDot = (tooltipMaxAngleFromCamera - 90.0f) / -90.0f;
|
|
if (distToFace < fadeDistanceFromFace
|
|
|| distToFace > tooltipMinDistanceFromFace
|
|
|| dot < minDot) {
|
|
tooltipAlphaValue = Mathf.Max(0.0f, tooltipAlphaValue - animationDelta);
|
|
} else {
|
|
tooltipAlphaValue = Mathf.Min(1.0f, tooltipAlphaValue + animationDelta);
|
|
}
|
|
}
|
|
|
|
private void UpdatePointer() {
|
|
// Determine the direction of the ray.
|
|
pointerPosition = wristPosition + wristRotation * POINTER_OFFSET;
|
|
pointerRotation = wristRotation * Quaternion.AngleAxis(pointerTiltAngle, Vector3.right);
|
|
}
|
|
|
|
#endif // UNITY_HAS_GOOGLEVR && (UNITY_ANDROID || UNITY_EDITOR)
|
|
}
|