| // Copyright 2017 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 "device/vr/openvr/openvr_gamepad_helper.h" |
| #include "device/vr/openvr/openvr_api_wrapper.h" |
| |
| #include <memory> |
| #include <unordered_set> |
| |
| #include "base/compiler_specific.h" |
| #include "base/logging.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "device/gamepad/public/cpp/gamepads.h" |
| #include "device/vr/util/gamepad_builder.h" |
| #include "device/vr/vr_device.h" |
| #include "third_party/openvr/src/headers/openvr.h" |
| #include "ui/gfx/transform.h" |
| #include "ui/gfx/transform_util.h" |
| |
| namespace device { |
| |
| namespace { |
| |
| // Constants/functions used by both WebXR and WebVR. |
| constexpr double kJoystickDeadzone = 0.1; |
| |
| bool TryGetGamepadButton(const vr::VRControllerState_t& controller_state, |
| uint64_t supported_buttons, |
| vr::EVRButtonId button_id, |
| GamepadButton* button) { |
| uint64_t button_mask = vr::ButtonMaskFromId(button_id); |
| if ((supported_buttons & button_mask) != 0) { |
| bool button_pressed = (controller_state.ulButtonPressed & button_mask) != 0; |
| bool button_touched = (controller_state.ulButtonTouched & button_mask) != 0; |
| button->touched = button_touched || button_pressed; |
| button->pressed = button_pressed; |
| button->value = button_pressed ? 1.0 : 0.0; |
| return true; |
| } |
| |
| return false; |
| } |
| |
| vr::EVRButtonId GetAxisId(uint32_t axis_offset) { |
| return static_cast<vr::EVRButtonId>(vr::k_EButton_Axis0 + axis_offset); |
| } |
| |
| std::map<vr::EVRButtonId, GamepadBuilder::ButtonData> GetAxesButtons( |
| vr::IVRSystem* vr_system, |
| const vr::VRControllerState_t& controller_state, |
| uint64_t supported_buttons, |
| uint32_t controller_id) { |
| std::map<vr::EVRButtonId, GamepadBuilder::ButtonData> button_data_map; |
| |
| for (uint32_t j = 0; j < vr::k_unControllerStateAxisCount; ++j) { |
| int32_t axis_type = vr_system->GetInt32TrackedDeviceProperty( |
| controller_id, |
| static_cast<vr::TrackedDeviceProperty>(vr::Prop_Axis0Type_Int32 + j)); |
| |
| GamepadBuilder::ButtonData button_data; |
| double x_axis = controller_state.rAxis[j].x; |
| double y_axis = controller_state.rAxis[j].y; |
| |
| switch (axis_type) { |
| case vr::k_eControllerAxis_Joystick: |
| // We only want to apply the deadzone to joysticks, since various |
| // runtimes may not have already done that, but touchpads should |
| // be fine. |
| x_axis = std::fabs(x_axis) < kJoystickDeadzone ? 0 : x_axis; |
| y_axis = std::fabs(y_axis) < kJoystickDeadzone ? 0 : y_axis; |
| FALLTHROUGH; |
| case vr::k_eControllerAxis_TrackPad: { |
| button_data.has_both_axes = true; |
| button_data.x_axis = x_axis; |
| button_data.y_axis = y_axis; |
| vr::EVRButtonId button_id = GetAxisId(j); |
| GamepadButton button; |
| if (TryGetGamepadButton(controller_state, supported_buttons, button_id, |
| &button)) { |
| button_data.touched = button.touched; |
| button_data.pressed = button.pressed; |
| button_data.value = button.value; |
| button_data_map[button_id] = button_data; |
| } |
| } break; |
| case vr::k_eControllerAxis_Trigger: { |
| GamepadButton button; |
| GamepadBuilder::ButtonData button_data; |
| vr::EVRButtonId button_id = GetAxisId(j); |
| if (TryGetGamepadButton(controller_state, supported_buttons, button_id, |
| &button)) { |
| button_data.touched = button.touched; |
| button_data.pressed = button.pressed; |
| button_data.value = x_axis; |
| button_data_map[button_id] = button_data; |
| } |
| } break; |
| } |
| } |
| |
| return button_data_map; |
| } |
| |
| // Constants/functions only used by WebXR. |
| constexpr std::array<vr::EVRButtonId, 5> kWebXRButtonOrder = { |
| vr::k_EButton_A, vr::k_EButton_DPad_Left, vr::k_EButton_DPad_Up, |
| vr::k_EButton_DPad_Right, vr::k_EButton_DPad_Down, |
| }; |
| |
| // Constants/functions only used by WebVR. |
| constexpr std::array<vr::EVRButtonId, 7> kWebVRButtonOrder = { |
| vr::k_EButton_A, |
| vr::k_EButton_Grip, |
| vr::k_EButton_ApplicationMenu, |
| vr::k_EButton_DPad_Left, |
| vr::k_EButton_DPad_Up, |
| vr::k_EButton_DPad_Right, |
| vr::k_EButton_DPad_Down, |
| }; |
| |
| mojom::XRGamepadButtonPtr GetMojomGamepadButton( |
| const GamepadBuilder::ButtonData& data) { |
| auto ret = mojom::XRGamepadButton::New(); |
| ret->touched = data.touched; |
| ret->pressed = data.pressed; |
| ret->value = data.value; |
| |
| return ret; |
| } |
| |
| mojom::XRGamepadButtonPtr GetMojomGamepadButton(const GamepadButton& data) { |
| auto ret = mojom::XRGamepadButton::New(); |
| ret->touched = data.touched; |
| ret->pressed = data.pressed; |
| ret->value = data.value; |
| |
| return ret; |
| } |
| |
| } // namespace |
| |
| // WebVR Gamepad Getter. |
| mojom::XRGamepadDataPtr OpenVRGamepadHelper::GetGamepadData( |
| vr::IVRSystem* vr_system) { |
| mojom::XRGamepadDataPtr ret = mojom::XRGamepadData::New(); |
| |
| vr::TrackedDevicePose_t tracked_devices_poses[vr::k_unMaxTrackedDeviceCount]; |
| vr_system->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseSeated, 0.0f, |
| tracked_devices_poses, |
| vr::k_unMaxTrackedDeviceCount); |
| for (uint32_t i = 0; i < vr::k_unMaxTrackedDeviceCount; ++i) { |
| if (vr_system->GetTrackedDeviceClass(i) != |
| vr::TrackedDeviceClass_Controller) |
| continue; |
| |
| vr::VRControllerState_t controller_state; |
| bool have_state = vr_system->GetControllerState(i, &controller_state, |
| sizeof(controller_state)); |
| if (!have_state) |
| continue; |
| |
| auto gamepad = mojom::XRGamepad::New(); |
| gamepad->controller_id = i; |
| |
| gamepad->timestamp = base::TimeTicks::Now(); |
| |
| vr::ETrackedControllerRole hand = |
| vr_system->GetControllerRoleForTrackedDeviceIndex(i); |
| switch (hand) { |
| case vr::TrackedControllerRole_Invalid: |
| gamepad->hand = device::mojom::XRHandedness::NONE; |
| break; |
| case vr::TrackedControllerRole_LeftHand: |
| gamepad->hand = device::mojom::XRHandedness::LEFT; |
| break; |
| case vr::TrackedControllerRole_RightHand: |
| gamepad->hand = device::mojom::XRHandedness::RIGHT; |
| break; |
| } |
| |
| uint64_t supported_buttons = vr_system->GetUint64TrackedDeviceProperty( |
| i, vr::Prop_SupportedButtons_Uint64); |
| |
| std::map<vr::EVRButtonId, GamepadBuilder::ButtonData> button_data_map = |
| GetAxesButtons(vr_system, controller_state, supported_buttons, i); |
| |
| for (const auto& button_data_pair : button_data_map) { |
| GamepadBuilder::ButtonData data = button_data_pair.second; |
| |
| gamepad->buttons.push_back(GetMojomGamepadButton(data)); |
| if (data.has_both_axes) { |
| gamepad->axes.push_back(data.x_axis); |
| gamepad->axes.push_back(data.y_axis); |
| } |
| } |
| |
| for (const auto& button : kWebVRButtonOrder) { |
| GamepadButton data; |
| if (TryGetGamepadButton(controller_state, supported_buttons, button, |
| &data)) { |
| gamepad->buttons.push_back(GetMojomGamepadButton(data)); |
| } |
| } |
| |
| const vr::TrackedDevicePose_t& pose = tracked_devices_poses[i]; |
| if (pose.bPoseIsValid) { |
| const vr::HmdMatrix34_t& mat = pose.mDeviceToAbsoluteTracking; |
| gfx::Transform transform( |
| mat.m[0][0], mat.m[0][1], mat.m[0][2], mat.m[0][3], mat.m[1][0], |
| mat.m[1][1], mat.m[1][2], mat.m[1][3], mat.m[2][0], mat.m[2][1], |
| mat.m[2][2], mat.m[2][3], 0, 0, 0, 1); |
| |
| gfx::DecomposedTransform src_pose; |
| gfx::DecomposeTransform(&src_pose, transform); |
| auto dst_pose = mojom::VRPose::New(); |
| |
| dst_pose->orientation = std::vector<float>( |
| {src_pose.quaternion.x(), src_pose.quaternion.y(), |
| src_pose.quaternion.z(), src_pose.quaternion.w()}); |
| dst_pose->position = |
| std::vector<float>({src_pose.translate[0], src_pose.translate[1], |
| src_pose.translate[2]}); |
| dst_pose->angularVelocity = std::vector<float>( |
| {pose.vAngularVelocity.v[0], pose.vAngularVelocity.v[1], |
| pose.vAngularVelocity.v[2]}); |
| dst_pose->linearVelocity = std::vector<float>( |
| {pose.vVelocity.v[0], pose.vVelocity.v[1], pose.vVelocity.v[2]}); |
| |
| gamepad->pose = std::move(dst_pose); |
| } |
| |
| ret->gamepads.push_back(std::move(gamepad)); |
| } |
| |
| return ret; |
| } |
| |
| // Helper classes and WebXR Getters |
| class OpenVRGamepadBuilder : public GamepadBuilder { |
| public: |
| enum class AxesRequirement { |
| kOptional = 0, |
| kRequired = 1, |
| }; |
| |
| OpenVRGamepadBuilder(vr::IVRSystem* vr_system, |
| uint32_t controller_id, |
| vr::VRControllerState_t controller_state, |
| device::mojom::XRHandedness handedness) |
| : GamepadBuilder(GetGamepadId(vr_system, controller_id), |
| GamepadMapping::kXRStandard, |
| handedness), |
| controller_state_(controller_state) { |
| supported_buttons_ = vr_system->GetUint64TrackedDeviceProperty( |
| controller_id, vr::Prop_SupportedButtons_Uint64); |
| |
| axes_data_ = GetAxesButtons(vr_system, controller_state_, |
| supported_buttons_, controller_id); |
| } |
| |
| ~OpenVRGamepadBuilder() override = default; |
| |
| bool TryAddAxesButton( |
| vr::EVRButtonId button_id, |
| AxesRequirement requirement = AxesRequirement::kOptional) { |
| if (!IsInAxesData(button_id)) |
| return false; |
| |
| bool require_axes = (requirement == AxesRequirement::kRequired); |
| if (require_axes && !axes_data_[button_id].has_both_axes) |
| return false; |
| |
| AddButton(axes_data_[button_id]); |
| used_axes_.insert(button_id); |
| |
| return true; |
| } |
| |
| bool TryAddNextUnusedAxesButton() { |
| for (const auto& axes_data_pair : axes_data_) { |
| vr::EVRButtonId button_id = axes_data_pair.first; |
| if (IsUsed(button_id)) |
| continue; |
| |
| if (TryAddAxesButton(button_id, AxesRequirement::kRequired)) |
| return true; |
| } |
| |
| return false; |
| } |
| |
| bool TryAddButton(vr::EVRButtonId button_id) { |
| GamepadButton button; |
| if (TryGetGamepadButton(controller_state_, supported_buttons_, button_id, |
| &button)) { |
| AddButton(button); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| // This will add any remaining unused values from axes_data to the gamepad. |
| void AddRemainingAxes() { |
| for (const auto& axes_data_pair : axes_data_) { |
| if (!IsUsed(axes_data_pair.first)) |
| AddButton(axes_data_pair.second); |
| } |
| } |
| |
| private: |
| static bool IsControllerHTCVive(vr::IVRSystem* vr_system, |
| uint32_t controller_id) { |
| std::string model = |
| GetOpenVRString(vr_system, vr::Prop_ModelNumber_String, controller_id); |
| std::string manufacturer = GetOpenVRString( |
| vr_system, vr::Prop_ManufacturerName_String, controller_id); |
| return (model == "Vive Controller MV") && (manufacturer == "HTC"); |
| } |
| |
| // TODO(https://crbug.com/942201): Get correct ID string once WebXR spec issue |
| // #550 (https://github.com/immersive-web/webxr/issues/550) is resolved. |
| static std::string GetGamepadId(vr::IVRSystem* vr_system, |
| uint32_t controller_id) { |
| if (IsControllerHTCVive(vr_system, controller_id)) { |
| return "htc-vive"; |
| } |
| |
| return "openvr"; |
| } |
| |
| bool IsUsed(vr::EVRButtonId button_id) { |
| auto it = used_axes_.find(button_id); |
| return it != used_axes_.end(); |
| } |
| |
| bool IsInAxesData(vr::EVRButtonId button_id) { |
| auto it = axes_data_.find(button_id); |
| return it != axes_data_.end(); |
| } |
| |
| const vr::VRControllerState_t controller_state_; |
| uint64_t supported_buttons_; |
| std::map<vr::EVRButtonId, ButtonData> axes_data_; |
| std::unordered_set<vr::EVRButtonId> used_axes_; |
| |
| DISALLOW_COPY_AND_ASSIGN(OpenVRGamepadBuilder); |
| }; |
| |
| base::Optional<Gamepad> OpenVRGamepadHelper::GetXRGamepad( |
| vr::IVRSystem* vr_system, |
| uint32_t controller_id, |
| vr::VRControllerState_t controller_state, |
| device::mojom::XRHandedness handedness) { |
| OpenVRGamepadBuilder builder(vr_system, controller_id, controller_state, |
| handedness); |
| |
| if (!builder.TryAddAxesButton(vr::k_EButton_SteamVR_Trigger)) |
| return base::nullopt; |
| |
| if (!builder.TryAddNextUnusedAxesButton()) |
| return base::nullopt; |
| |
| if (!builder.TryAddButton(vr::k_EButton_Grip)) |
| builder.AddPlaceholderButton(); |
| |
| // If we can't find any secondary button with an x and y axis, add a fake |
| // button. Note that we're not worried about ensuring that the axes data gets |
| // added, because if there were any other axes to add, we would've added them. |
| if (!builder.TryAddNextUnusedAxesButton()) |
| builder.AddPlaceholderButton(); |
| |
| // Now that all of the xr-standard reserved buttons have been filled in, we |
| // add the rest of the buttons in order of decreasing importance. |
| // First add regular buttons |
| for (const auto& button : kWebXRButtonOrder) { |
| builder.TryAddButton(button); |
| } |
| |
| // Finally, add any remaining axis buttons (triggers/josysticks/touchpads) |
| builder.AddRemainingAxes(); |
| |
| return builder.GetGamepad(); |
| } |
| |
| } // namespace device |