blob: bfc32954a31c246fa48218de25546e42352a7f9e [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/android/vr_shell/vr_controller.h"
#include <algorithm>
#include <utility>
#include "base/logging.h"
#include "base/numerics/math_constants.h"
#include "base/numerics/ranges.h"
#include "third_party/WebKit/public/platform/WebGestureEvent.h"
#include "third_party/WebKit/public/platform/WebInputEvent.h"
#include "third_party/gvr-android-sdk/src/libraries/headers/vr/gvr/capi/include/gvr.h"
#include "third_party/gvr-android-sdk/src/libraries/headers/vr/gvr/capi/include/gvr_controller.h"
#include "ui/gfx/transform.h"
namespace vr_shell {
namespace {
constexpr float kDisplacementScaleFactor = 300.0f;
// A slop represents a small rectangular region around the first touch point of
// a gesture.
// If the user does not move outside of the slop, no gesture is detected.
// Gestures start to be detected when the user moves outside of the slop.
// Vertical distance from the border to the center of slop.
constexpr float kSlopVertical = 0.185f;
// Horizontal distance from the border to the center of slop.
constexpr float kSlopHorizontal = 0.17f;
// Minimum distance needed in at least one direction to call two vectors
// not equal. Also, minimum time distance needed to call two timestamps
// not equal.
constexpr float kDelta = 1.0e-7f;
constexpr float kCutoffHz = 10.0f;
constexpr float kRC = 1.0f / (2.0f * base::kPiFloat * kCutoffHz);
constexpr float kNanoSecondsPerSecond = 1.0e9f;
constexpr int kMaxNumOfExtrapolations = 2;
// Distance from the center of the controller to start rendering the laser.
constexpr float kLaserStartDisplacement = 0.045;
constexpr float kFadeDistanceFromFace = 0.34f;
constexpr float kDeltaAlpha = 3.0f;
void ClampTouchpadPosition(gfx::Vector2dF* position) {
position->set_x(base::ClampToRange(position->x(), 0.0f, 1.0f));
position->set_y(base::ClampToRange(position->y(), 0.0f, 1.0f));
}
float DeltaTimeSeconds(int64_t last_timestamp_nanos) {
return (gvr::GvrApi::GetTimePointNow().monotonic_system_time_nanos -
last_timestamp_nanos) /
kNanoSecondsPerSecond;
}
gvr::ControllerButton PlatformToGvrButton(
vr::PlatformController::ButtonType type) {
switch (type) {
case vr::PlatformController::kButtonHome:
return gvr::kControllerButtonHome;
case vr::PlatformController::kButtonMenu:
return gvr::kControllerButtonApp;
case vr::PlatformController::kButtonSelect:
return gvr::kControllerButtonClick;
default:
return gvr::kControllerButtonNone;
}
}
} // namespace
VrController::VrController(gvr_context* gvr_context) {
DVLOG(1) << __FUNCTION__ << "=" << this;
CHECK(gvr_context != nullptr) << "invalid gvr_context";
controller_api_ = std::make_unique<gvr::ControllerApi>();
controller_state_ = std::make_unique<gvr::ControllerState>();
gvr_api_ = gvr::GvrApi::WrapNonOwned(gvr_context);
int32_t options = gvr::ControllerApi::DefaultOptions();
options |= GVR_CONTROLLER_ENABLE_ARM_MODEL;
// Enable non-default options - WebVR needs gyro and linear acceleration, and
// since VrShell implements GvrGamepadDataProvider we need this always.
options |= GVR_CONTROLLER_ENABLE_GYRO;
options |= GVR_CONTROLLER_ENABLE_ACCEL;
CHECK(controller_api_->Init(options, gvr_context));
controller_api_->Resume();
handedness_ = gvr_api_->GetUserPrefs().GetControllerHandedness();
Reset();
last_timestamp_nanos_ =
gvr::GvrApi::GetTimePointNow().monotonic_system_time_nanos;
}
VrController::~VrController() {
DVLOG(1) << __FUNCTION__ << "=" << this;
}
void VrController::OnResume() {
if (controller_api_) {
controller_api_->Resume();
handedness_ = gvr_api_->GetUserPrefs().GetControllerHandedness();
}
}
void VrController::OnPause() {
if (controller_api_)
controller_api_->Pause();
}
device::GvrGamepadData VrController::GetGamepadData() {
device::GvrGamepadData pad = {};
pad.connected = IsConnected();
pad.timestamp = controller_state_->GetLastOrientationTimestamp();
if (pad.connected) {
pad.touch_pos.set_x(TouchPosX());
pad.touch_pos.set_y(TouchPosY());
pad.orientation = Orientation();
// Use orientation to rotate acceleration/gyro into seated space.
gfx::Transform pose_mat(Orientation());
const gvr::Vec3f& accel = controller_state_->GetAccel();
const gvr::Vec3f& gyro = controller_state_->GetGyro();
pad.accel = gfx::Vector3dF(accel.x, accel.y, accel.z);
pose_mat.TransformVector(&pad.accel);
pad.gyro = gfx::Vector3dF(gyro.x, gyro.y, gyro.z);
pose_mat.TransformVector(&pad.gyro);
pad.is_touching = controller_state_->IsTouching();
pad.controller_button_pressed =
controller_state_->GetButtonState(GVR_CONTROLLER_BUTTON_CLICK);
pad.right_handed = handedness_ == GVR_CONTROLLER_RIGHT_HANDED;
}
return pad;
}
bool VrController::IsTouching() {
return controller_state_->IsTouching();
}
float VrController::TouchPosX() {
return controller_state_->GetTouchPos().x;
}
float VrController::TouchPosY() {
return controller_state_->GetTouchPos().y;
}
bool VrController::IsButtonDown(vr::PlatformController::ButtonType type) const {
return controller_state_->GetButtonState(PlatformToGvrButton(type));
}
base::TimeTicks VrController::GetLastOrientationTimestamp() const {
// controller_state_->GetLast*Timestamp() returns timestamps in a
// different timebase from base::TimeTicks::Now(), so we can't use the
// timestamps in any meaningful way in the rest of Chrome.
// TODO(mthiesse): Use controller_state_->GetLastOrientationTimestamp() when
// b/62818778 is resolved.
return base::TimeTicks::Now();
}
base::TimeTicks VrController::GetLastTouchTimestamp() const {
// TODO(mthiesse): Use controller_state_->GetLastTouchTimestamp() when
// b/62818778 is resolved.
return base::TimeTicks::Now();
}
base::TimeTicks VrController::GetLastButtonTimestamp() const {
// TODO(mthiesse): Use controller_state_->GetLastButtonTimestamp() when
// b/62818778 is resolved.
return base::TimeTicks::Now();
}
vr::PlatformController::Handedness VrController::GetHandedness() const {
return handedness_ == GVR_CONTROLLER_RIGHT_HANDED
? vr::PlatformController::kRightHanded
: vr::PlatformController::kLeftHanded;
}
gfx::Quaternion VrController::Orientation() const {
const gvr::Quatf& orientation = controller_state_->GetOrientation();
return gfx::Quaternion(orientation.qx, orientation.qy, orientation.qz,
orientation.qw);
}
gfx::Point3F VrController::Position() const {
gvr::Vec3f position = controller_state_->GetPosition();
return gfx::Point3F(position.x, position.y, position.z);
}
void VrController::GetTransform(gfx::Transform* out) const {
*out = gfx::Transform(Orientation());
gvr::Vec3f position = controller_state_->GetPosition();
out->matrix().postTranslate(position.x, position.y, position.z);
}
float VrController::GetOpacity() const {
return alpha_value_;
}
gfx::Point3F VrController::GetPointerStart() const {
gfx::Vector3dF pointer_direction{0.0f, -sin(kErgoAngleOffset),
-cos(kErgoAngleOffset)};
gfx::Transform rotation_mat(Orientation());
rotation_mat.TransformVector(&pointer_direction);
return Position() +
gfx::ScaleVector3d(pointer_direction, kLaserStartDisplacement);
}
bool VrController::TouchDownHappened() {
return controller_state_->GetTouchDown();
}
bool VrController::TouchUpHappened() {
return controller_state_->GetTouchUp();
}
bool VrController::ButtonDownHappened(gvr::ControllerButton button) {
return controller_state_->GetButtonDown(button);
}
bool VrController::ButtonUpHappened(gvr::ControllerButton button) {
return controller_state_->GetButtonUp(button);
}
bool VrController::ButtonState(gvr::ControllerButton button) const {
return controller_state_->GetButtonState(button);
}
bool VrController::IsConnected() {
return controller_state_->GetConnectionState() == gvr::kControllerConnected;
}
void VrController::UpdateState(const gvr::Mat4f& head_direction) {
controller_api_->ApplyArmModel(handedness_, gvr::kArmModelBehaviorFollowGaze,
head_direction);
const int32_t old_status = controller_state_->GetApiStatus();
const int32_t old_connection_state = controller_state_->GetConnectionState();
// Read current controller state.
controller_state_->Update(*controller_api_);
// Print new API status and connection state, if they changed.
if (controller_state_->GetApiStatus() != old_status ||
controller_state_->GetConnectionState() != old_connection_state) {
VLOG(1) << "Controller Connection status: "
<< gvr_controller_connection_state_to_string(
controller_state_->GetConnectionState());
}
UpdateAlpha();
}
void VrController::UpdateTouchInfo() {
CHECK(touch_info_ != nullptr) << "touch_info_ not initialized properly.";
if (IsTouching() && state_ == SCROLLING &&
(controller_state_->GetLastTouchTimestamp() == last_touch_timestamp_ ||
(cur_touch_point_->position == prev_touch_point_->position &&
extrapolated_touch_ < kMaxNumOfExtrapolations))) {
extrapolated_touch_++;
touch_position_changed_ = true;
// vr::Fill the touch_info
float duration = DeltaTimeSeconds(last_timestamp_nanos_);
touch_info_->touch_point.position.set_x(cur_touch_point_->position.x() +
overall_velocity_.x() * duration);
touch_info_->touch_point.position.set_y(cur_touch_point_->position.y() +
overall_velocity_.y() * duration);
} else {
if (extrapolated_touch_ == kMaxNumOfExtrapolations) {
overall_velocity_ = {0, 0};
}
extrapolated_touch_ = 0;
}
last_touch_timestamp_ = controller_state_->GetLastTouchTimestamp();
last_timestamp_nanos_ =
gvr::GvrApi::GetTimePointNow().monotonic_system_time_nanos;
}
std::unique_ptr<GestureList> VrController::DetectGestures() {
std::unique_ptr<GestureList> gesture_list = std::make_unique<GestureList>();
std::unique_ptr<blink::WebGestureEvent> gesture(new blink::WebGestureEvent());
if (controller_state_->GetConnectionState() != gvr::kControllerConnected) {
gesture_list->push_back(std::move(gesture));
return gesture_list;
}
touch_position_changed_ = UpdateCurrentTouchpoint();
UpdateTouchInfo();
if (touch_position_changed_)
UpdateOverallVelocity();
UpdateGestureFromTouchInfo(gesture.get());
gesture->source_device = blink::kWebGestureDeviceTouchpad;
gesture_list->push_back(std::move(gesture));
if (gesture_list->back()->GetType() ==
blink::WebInputEvent::kGestureScrollEnd) {
if (!ButtonDownHappened(gvr::kControllerButtonClick) &&
(last_velocity_.x() != 0.0 || last_velocity_.y() != 0.0)) {
std::unique_ptr<blink::WebGestureEvent> fling(
new blink::WebGestureEvent(blink::WebInputEvent::kGestureFlingStart,
blink::WebInputEvent::kNoModifiers,
gesture_list->back()->TimeStampSeconds()));
fling->source_device = blink::kWebGestureDeviceTouchpad;
if (IsHorizontalGesture()) {
fling->data.fling_start.velocity_x =
last_velocity_.x() * kDisplacementScaleFactor;
} else {
fling->data.fling_start.velocity_y =
last_velocity_.y() * kDisplacementScaleFactor;
}
// FlingStart replaces ScrollEnd
gesture_list->pop_back();
gesture_list->push_back(std::move(fling));
}
Reset();
}
return gesture_list;
}
void VrController::UpdateGestureFromTouchInfo(blink::WebGestureEvent* gesture) {
gesture->SetTimeStampSeconds(
(GetLastTouchTimestamp() - base::TimeTicks()).InSecondsF());
switch (state_) {
// User has not put finger on touch pad.
case WAITING:
HandleWaitingState(gesture);
break;
// User has not started a gesture (by moving out of slop).
case TOUCHING:
HandleDetectingState(gesture);
break;
// User is scrolling on touchpad
case SCROLLING:
HandleScrollingState(gesture);
break;
default:
NOTREACHED();
break;
}
}
void VrController::HandleWaitingState(blink::WebGestureEvent* gesture) {
// User puts finger on touch pad (or when the touch down for current gesture
// is missed, initiate gesture from current touch point).
if (touch_info_->touch_down || touch_info_->is_touching) {
// update initial touchpoint
*init_touch_point_ = touch_info_->touch_point;
// update current touchpoint
*cur_touch_point_ = touch_info_->touch_point;
state_ = TOUCHING;
gesture->SetType(blink::WebInputEvent::kGestureFlingCancel);
gesture->data.fling_cancel.prevent_boosting = false;
}
}
void VrController::HandleDetectingState(blink::WebGestureEvent* gesture) {
// User lifts up finger from touch pad.
if (touch_info_->touch_up || !(touch_info_->is_touching)) {
Reset();
return;
}
// Touch position is changed, the touch point moves outside of slop,
// and the Controller's button is not down.
if (touch_position_changed_ && touch_info_->is_touching &&
!InSlop(touch_info_->touch_point.position) &&
!ButtonState(gvr::kControllerButtonClick)) {
state_ = SCROLLING;
gesture->SetType(blink::WebInputEvent::kGestureScrollBegin);
UpdateGestureParameters();
gesture->data.scroll_begin.delta_x_hint =
displacement_.x() * kDisplacementScaleFactor;
gesture->data.scroll_begin.delta_y_hint =
displacement_.y() * kDisplacementScaleFactor;
gesture->data.scroll_begin.delta_hint_units =
blink::WebGestureEvent::ScrollUnits::kPrecisePixels;
}
}
void VrController::HandleScrollingState(blink::WebGestureEvent* gesture) {
if (touch_info_->touch_up || !(touch_info_->is_touching) ||
ButtonDownHappened(gvr::kControllerButtonClick)) {
// Gesture ends.
gesture->SetType(blink::WebInputEvent::kGestureScrollEnd);
UpdateGestureParameters();
} else if (touch_position_changed_) {
// User continues scrolling and there is a change in touch position.
gesture->SetType(blink::WebInputEvent::kGestureScrollUpdate);
UpdateGestureParameters();
if (IsHorizontalGesture()) {
gesture->data.scroll_update.delta_x =
displacement_.x() * kDisplacementScaleFactor;
} else {
gesture->data.scroll_update.delta_y =
displacement_.y() * kDisplacementScaleFactor;
}
last_velocity_ = overall_velocity_;
}
}
bool VrController::IsHorizontalGesture() {
return std::abs(last_velocity_.x()) > std::abs(last_velocity_.y());
}
bool VrController::InSlop(const gfx::Vector2dF touch_position) {
return (std::abs(touch_position.x() - init_touch_point_->position.x()) <
kSlopHorizontal) &&
(std::abs(touch_position.y() - init_touch_point_->position.y()) <
kSlopVertical);
}
void VrController::Reset() {
// Reset state.
state_ = WAITING;
// Reset the pointers.
prev_touch_point_.reset(new TouchPoint);
cur_touch_point_.reset(new TouchPoint);
init_touch_point_.reset(new TouchPoint);
touch_info_.reset(new TouchInfo);
overall_velocity_ = {0, 0};
last_velocity_ = {0, 0};
}
void VrController::UpdateGestureParameters() {
displacement_ =
touch_info_->touch_point.position - prev_touch_point_->position;
}
bool VrController::UpdateCurrentTouchpoint() {
touch_info_->touch_up = TouchUpHappened();
touch_info_->touch_down = TouchDownHappened();
touch_info_->is_touching = IsTouching();
touch_info_->touch_point.position.set_x(TouchPosX());
touch_info_->touch_point.position.set_y(TouchPosY());
ClampTouchpadPosition(&touch_info_->touch_point.position);
touch_info_->touch_point.timestamp =
gvr::GvrApi::GetTimePointNow().monotonic_system_time_nanos;
if (IsTouching() || TouchUpHappened()) {
// Update the touch point when the touch position has changed.
if (cur_touch_point_->position != touch_info_->touch_point.position) {
prev_touch_point_.swap(cur_touch_point_);
cur_touch_point_.reset(new TouchPoint);
cur_touch_point_->position = touch_info_->touch_point.position;
cur_touch_point_->timestamp = touch_info_->touch_point.timestamp;
return true;
}
}
return false;
}
void VrController::UpdateOverallVelocity() {
float duration =
(touch_info_->touch_point.timestamp - prev_touch_point_->timestamp) /
kNanoSecondsPerSecond;
// If the timestamp does not change, do not update velocity.
if (duration < kDelta)
return;
const gfx::Vector2dF& displacement =
touch_info_->touch_point.position - prev_touch_point_->position;
const gfx::Vector2dF& velocity = ScaleVector2d(displacement, (1 / duration));
float weight = duration / (kRC + duration);
overall_velocity_ = ScaleVector2d(overall_velocity_, (1 - weight)) +
ScaleVector2d(velocity, weight);
}
void VrController::UpdateAlpha() {
float distance_to_face = (Position() - gfx::Point3F()).Length();
float alpha_change = kDeltaAlpha * DeltaTimeSeconds(last_timestamp_nanos_);
alpha_value_ = base::ClampToRange(distance_to_face < kFadeDistanceFromFace
? alpha_value_ - alpha_change
: alpha_value_ + alpha_change,
0.0f, 1.0f);
}
} // namespace vr_shell