Skip to content

Commit

Permalink
Version 7 fixes an issue with how the Quaternion is calculated that m…
Browse files Browse the repository at this point in the history
…ade it unusable. Sorry about that -- none of my uses of GamepadMotionHelpers actually use the Quaternion, as I find Gravity and Gyro more useful for my particular uses.

Added GetPlayerSpaceGyro and GetWorldSpaceGyro helper functions to calculate Player Space or World Space gyro input, respectively. Outputs X and Y axes, and you can treat Z as 0. Counterintuitively, X is vertical "movement" and Y is horizontal "movement", but this is because they're still rotations, and that makes these a drop-in replacement for wherever you're using GetCalibratedGyro. I recommend Player Space, but some games use World instead (it's been around for longer) and there are some trade-offs between the two. Read more at GyroWiki: http://gyrowiki.jibbsmart.com/blog:player-space-gyro-and-alternatives-explained
  • Loading branch information
JibbSmart committed Mar 6, 2023
1 parent 92f9f36 commit 689a9b8
Show file tree
Hide file tree
Showing 2 changed files with 50 additions and 3 deletions.
48 changes: 45 additions & 3 deletions GamepadMotion.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright (c) 2020-2021 Julian "Jibb" Smart
// Copyright (c) 2020-2023 Julian "Jibb" Smart
// Released under the MIT license. See https://github.com/JibbSmart/GamepadMotionHelpers/blob/main/LICENSE for more info
// Version 6
// Version 7

#pragma once

Expand Down Expand Up @@ -233,6 +233,8 @@ class GamepadMotion
void GetGravity(float& x, float& y, float& z);
void GetProcessedAcceleration(float& x, float& y, float& z);
void GetOrientation(float& w, float& x, float& y, float& z);
void GetPlayerSpaceGyro(float& x, float& y, const float yawRelaxFactor = 1.41f);
void GetWorldSpaceGyro(float& x, float& y, const float sideReductionThreshold = 0.125f);

// gyro calibration functions
void StartContinuousCalibration();
Expand Down Expand Up @@ -630,7 +632,7 @@ namespace GamepadMotionHelpers
const float errorAngle = acosf(std::clamp(Vec(0.0f, -1.0f, 0.0f).Dot(gravityDirection), -1.f, 1.f));
const Vec flattened = Vec(0.0f, -1.0f, 0.0f).Cross(gravityDirection);
Quat correctionQuat = AngleAxis(errorAngle, flattened.x, flattened.y, flattened.z);
Quaternion = correctionQuat * Quaternion;
Quaternion = Quaternion * correctionQuat;

Accel = accel + Grav;
}
Expand Down Expand Up @@ -1119,6 +1121,46 @@ void GamepadMotion::GetOrientation(float& w, float& x, float& y, float& z)
z = Motion.Quaternion.z;
}

void GamepadMotion::GetPlayerSpaceGyro(float& x, float& y, const float yawRelaxFactor)
{
// take gravity into account without taking on any error from gravity. Explained in depth at http://gyrowiki.jibbsmart.com/blog:player-space-gyro-and-alternatives-explained#toc7
const float worldYaw = -(Motion.Grav.y * Gyro.y + Motion.Grav.z * Gyro.z);
const float worldYawSign = worldYaw < 0.f ? -1.f : 1.f;
y = worldYawSign * std::min(std::abs(worldYaw) * yawRelaxFactor, sqrtf(Gyro.y * Gyro.y + Gyro.z * Gyro.z));
x = Gyro.x;
}

void GamepadMotion::GetWorldSpaceGyro(float& x, float& y, const float sideReductionThreshold)
{
// use the gravity direction as the yaw axis, and derive an appropriate pitch axis. Explained in depth at http://gyrowiki.jibbsmart.com/blog:player-space-gyro-and-alternatives-explained#toc6
const float worldYaw = -Motion.Grav.Dot(Gyro);
// project local pitch axis (X) onto gravity plane
const float gravDotPitchAxis = Motion.Grav.x;
GamepadMotionHelpers::Vec pitchAxis(1.f - Motion.Grav.x * gravDotPitchAxis,
-Motion.Grav.y * gravDotPitchAxis,
-Motion.Grav.z * gravDotPitchAxis);
// normalize
const float pitchAxisLengthSquared = pitchAxis.LengthSquared();
if (pitchAxisLengthSquared > 0.f)
{
const float pitchAxisLength = sqrtf(pitchAxisLengthSquared);
const float lengthReciprocal = 1.f / pitchAxisLength;
pitchAxis *= lengthReciprocal;

const float flatness = std::abs(Motion.Grav.y);
const float upness = std::abs(Motion.Grav.z);
const float sideReduction = sideReductionThreshold <= 0.f ? 1.f : std::clamp((std::max(flatness, upness) - sideReductionThreshold) / sideReductionThreshold, 0.f, 1.f);

x = sideReduction * pitchAxis.Dot(Gyro);
}
else
{
x = 0.f;
}

y = worldYaw;
}

// gyro calibration functions
void GamepadMotion::StartContinuousCalibration()
{
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ ProcessMotion takes these inputs, updates some internal values, and then you can
- ```GetProcessedAcceleration(float& x, float& y, float& z)``` - Get the controller's current acceleration in g-force with gravity removed. Raw accelerometer input includes gravity -- it is only (0, 0, 0) when the controller is in freefall. However, using the gravity direction as calculated for GetGravity, it can remove that component and detect how you're shaking the controller about. This function gives you that acceleration vector with the gravity removed.
- ```GetOrientation(float& w, float& x, float& y, float& z)``` - Get the controller's orientation. Gyro and accelerometer input are combined to give a good estimate of the controller's orientation.

Additional helper functions are available for taking gravity into account and returning a "world space" or "player space" rotation in two axes. Bear in mind that the **X** and **Y** set by these functions is still around the controller's axes. This means **Y** is the *horizontal* part of the rotation, and *x* is the vertical part. To convert to a mouse-like input, you'll treat the **Y** as the horizontal or yaw input and **X** as the vertical or pitch input. This might be unintuitive, but since it's also true of the "local space" angular velocities obtained from GetCalibratedGyro, this makes it simple to let the user choose between *local space*, *world space*, and *player space* in your game or application.
- ```void GetWorldSpaceGyro(float& x, float& y, const float sideReductionThreshold = 0.125f)``` - Get the controller's angular velocity in *world space* as described on GyroWiki in the [player space article here](http://gyrowiki.jibbsmart.com/blog:player-space-gyro-and-alternatives-explained#toc6). Yaw input will be derived from motion around the gravity axis, and pitch input from an appropriate pitch axis calculated from the controller's orientation with respect to the gravity axis. Any errors in the calculated gravity axis (though likely very small) will be taken on by the calculated world space gyro rotation, making it slightly less robust than using calibrated gyro directly ("local space" gyro) or using *player space* gyro below. More info in the linked article.
- ```GetPlayerSpaceGyro(float& x, float& y, const float yawRelaxFactor = 1.41f)``` - Get the controller's angular velocity in *player space* as described on GyroWiki in the [player space article here](http://gyrowiki.jibbsmart.com/blog:player-space-gyro-and-alternatives-explained#toc7). Yaw input will be derived from motion approximately around the gravity axis, without any impact from errors in the gravity calculation. Pitch is just local pitch. It is robust, accommodates players who are used to both local space and world space gyro, while taking on most of the advantages of each. It is proven in popular games and is an ideal default for players using a standalone controller. For handheld, local space (using the calibrated gyro input directly) may be preferable. More info in the linked article.

## Sensor Fusion
Combining multiple types of sensor like this to get a better picture of the controller's state is called "sensor fusion". Moment-to-moment changes in orientation are detected using the gyro, but that only gives local angular velocity and needs to be correctly calibrated. Errors can accumulate over time. The gravity vector as detected by the accelerometer is used to make corrections to the relevant components of the controller's orientation.

Expand Down Expand Up @@ -62,6 +66,7 @@ If you aren't sure what to choose, I'd suggest using the combined ```Calibration
## In the Wild
GamepadMotionHelpers is currently used in:
- [JoyShockMapper](https://github.com/Electronicks/JoyShockMapper)
- [JoyShockLibrary](https://github.com/JibbSmart/JoyShockLibrary)
- JoyShockOverlay

If you know of any other games or applications using GamepadMotionHelpers, please let me know!

0 comments on commit 689a9b8

Please sign in to comment.