1313 lines
39 KiB
C++
1313 lines
39 KiB
C++
|
// 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 9
|
||
|
|
||
|
#pragma once
|
||
|
|
||
|
#define _USE_MATH_DEFINES
|
||
|
#include <math.h>
|
||
|
#include <algorithm> // std::min, std::max and std::clamp
|
||
|
|
||
|
// You don't need to look at these. These will just be used internally by the GamepadMotion class declared below.
|
||
|
// You can ignore anything in namespace GamepadMotionHelpers.
|
||
|
class GamepadMotionSettings;
|
||
|
class GamepadMotion;
|
||
|
|
||
|
namespace GamepadMotionHelpers
|
||
|
{
|
||
|
struct GyroCalibration
|
||
|
{
|
||
|
float X;
|
||
|
float Y;
|
||
|
float Z;
|
||
|
float AccelMagnitude;
|
||
|
int NumSamples;
|
||
|
};
|
||
|
|
||
|
struct Quat
|
||
|
{
|
||
|
float w;
|
||
|
float x;
|
||
|
float y;
|
||
|
float z;
|
||
|
|
||
|
Quat();
|
||
|
Quat(float inW, float inX, float inY, float inZ);
|
||
|
void Set(float inW, float inX, float inY, float inZ);
|
||
|
Quat& operator*=(const Quat& rhs);
|
||
|
friend Quat operator*(Quat lhs, const Quat& rhs);
|
||
|
void Normalize();
|
||
|
Quat Normalized() const;
|
||
|
void Invert();
|
||
|
Quat Inverse() const;
|
||
|
};
|
||
|
|
||
|
struct Vec
|
||
|
{
|
||
|
float x;
|
||
|
float y;
|
||
|
float z;
|
||
|
|
||
|
Vec();
|
||
|
Vec(float inValue);
|
||
|
Vec(float inX, float inY, float inZ);
|
||
|
void Set(float inX, float inY, float inZ);
|
||
|
float Length() const;
|
||
|
float LengthSquared() const;
|
||
|
void Normalize();
|
||
|
Vec Normalized() const;
|
||
|
float Dot(const Vec& other) const;
|
||
|
Vec Cross(const Vec& other) const;
|
||
|
Vec Min(const Vec& other) const;
|
||
|
Vec Max(const Vec& other) const;
|
||
|
Vec Abs() const;
|
||
|
Vec Lerp(const Vec& other, float factor) const;
|
||
|
Vec Lerp(const Vec& other, const Vec& factor) const;
|
||
|
Vec& operator+=(const Vec& rhs);
|
||
|
friend Vec operator+(Vec lhs, const Vec& rhs);
|
||
|
Vec& operator-=(const Vec& rhs);
|
||
|
friend Vec operator-(Vec lhs, const Vec& rhs);
|
||
|
Vec& operator*=(const float rhs);
|
||
|
friend Vec operator*(Vec lhs, const float rhs);
|
||
|
Vec& operator/=(const float rhs);
|
||
|
friend Vec operator/(Vec lhs, const float rhs);
|
||
|
Vec& operator*=(const Quat& rhs);
|
||
|
friend Vec operator*(Vec lhs, const Quat& rhs);
|
||
|
Vec operator-() const;
|
||
|
};
|
||
|
|
||
|
struct SensorMinMaxWindow
|
||
|
{
|
||
|
Vec MinGyro;
|
||
|
Vec MaxGyro;
|
||
|
Vec MeanGyro;
|
||
|
Vec MinAccel;
|
||
|
Vec MaxAccel;
|
||
|
Vec MeanAccel;
|
||
|
Vec StartAccel;
|
||
|
int NumSamples = 0;
|
||
|
float TimeSampled = 0.f;
|
||
|
|
||
|
SensorMinMaxWindow();
|
||
|
void Reset(float remainder);
|
||
|
void AddSample(const Vec& inGyro, const Vec& inAccel, float deltaTime);
|
||
|
Vec GetMidGyro();
|
||
|
};
|
||
|
|
||
|
struct AutoCalibration
|
||
|
{
|
||
|
SensorMinMaxWindow MinMaxWindow;
|
||
|
Vec SmoothedAngularVelocityGyro;
|
||
|
Vec SmoothedAngularVelocityAccel;
|
||
|
Vec SmoothedPreviousAccel;
|
||
|
Vec PreviousAccel;
|
||
|
|
||
|
AutoCalibration();
|
||
|
void Reset();
|
||
|
bool AddSampleStillness(const Vec& inGyro, const Vec& inAccel, float deltaTime, bool doSensorFusion);
|
||
|
void NoSampleStillness();
|
||
|
bool AddSampleSensorFusion(const Vec& inGyro, const Vec& inAccel, float deltaTime);
|
||
|
void NoSampleSensorFusion();
|
||
|
void SetCalibrationData(GyroCalibration* calibrationData);
|
||
|
void SetSettings(GamepadMotionSettings* settings);
|
||
|
|
||
|
float Confidence = 0.f;
|
||
|
bool IsSteady() { return bIsSteady; }
|
||
|
|
||
|
private:
|
||
|
Vec MinDeltaGyro = Vec(1.f);
|
||
|
Vec MinDeltaAccel = Vec(0.25f);
|
||
|
float RecalibrateThreshold = 1.f;
|
||
|
float SensorFusionSkippedTime = 0.f;
|
||
|
float TimeSteadySensorFusion = 0.f;
|
||
|
float TimeSteadyStillness = 0.f;
|
||
|
bool bIsSteady = false;
|
||
|
|
||
|
GyroCalibration* CalibrationData;
|
||
|
GamepadMotionSettings* Settings;
|
||
|
};
|
||
|
|
||
|
struct Motion
|
||
|
{
|
||
|
Quat Quaternion;
|
||
|
Vec Accel;
|
||
|
Vec Grav;
|
||
|
|
||
|
Vec SmoothAccel = Vec();
|
||
|
float Shakiness = 0.f;
|
||
|
const float ShortSteadinessHalfTime = 0.25f;
|
||
|
const float LongSteadinessHalfTime = 1.f;
|
||
|
|
||
|
Motion();
|
||
|
void Reset();
|
||
|
void Update(float inGyroX, float inGyroY, float inGyroZ, float inAccelX, float inAccelY, float inAccelZ, float gravityLength, float deltaTime);
|
||
|
void SetSettings(GamepadMotionSettings* settings);
|
||
|
|
||
|
private:
|
||
|
GamepadMotionSettings* Settings;
|
||
|
};
|
||
|
|
||
|
enum CalibrationMode
|
||
|
{
|
||
|
Manual = 0,
|
||
|
Stillness = 1,
|
||
|
SensorFusion = 2,
|
||
|
};
|
||
|
|
||
|
// https://stackoverflow.com/a/1448478/1130520
|
||
|
inline CalibrationMode operator|(CalibrationMode a, CalibrationMode b)
|
||
|
{
|
||
|
return static_cast<CalibrationMode>(static_cast<int>(a) | static_cast<int>(b));
|
||
|
}
|
||
|
|
||
|
inline CalibrationMode operator&(CalibrationMode a, CalibrationMode b)
|
||
|
{
|
||
|
return static_cast<CalibrationMode>(static_cast<int>(a) & static_cast<int>(b));
|
||
|
}
|
||
|
|
||
|
inline CalibrationMode operator~(CalibrationMode a)
|
||
|
{
|
||
|
return static_cast<CalibrationMode>(~static_cast<int>(a));
|
||
|
}
|
||
|
|
||
|
// https://stackoverflow.com/a/23152590/1130520
|
||
|
inline CalibrationMode& operator|=(CalibrationMode& a, CalibrationMode b)
|
||
|
{
|
||
|
return (CalibrationMode&)((int&)(a) |= static_cast<int>(b));
|
||
|
}
|
||
|
|
||
|
inline CalibrationMode& operator&=(CalibrationMode& a, CalibrationMode b)
|
||
|
{
|
||
|
return (CalibrationMode&)((int&)(a) &= static_cast<int>(b));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Note that I'm using a Y-up coordinate system. This is to follow the convention set by the motion sensors in
|
||
|
// PlayStation controllers, which was what I was using when writing in this. But for the record, Z-up is
|
||
|
// better for most games (XY ground-plane in 3D games simplifies using 2D vectors in navigation, for example).
|
||
|
|
||
|
// Gyro units should be degrees per second. Accelerometer should be g-force (approx. 9.8 m/s^2 = 1 g). If you're using
|
||
|
// radians per second, meters per second squared, etc, conversion should be simple.
|
||
|
|
||
|
class GamepadMotionSettings
|
||
|
{
|
||
|
public:
|
||
|
int MinStillnessSamples = 10;
|
||
|
float MinStillnessCollectionTime = 0.5f;
|
||
|
float MinStillnessCorrectionTime = 2.f;
|
||
|
float MaxStillnessError = 2.f;
|
||
|
float StillnessSampleDeteriorationRate = 0.2f;
|
||
|
float StillnessErrorClimbRate = 0.1f;
|
||
|
float StillnessErrorDropOnRecalibrate = 0.1f;
|
||
|
float StillnessCalibrationEaseInTime = 3.f;
|
||
|
float StillnessCalibrationHalfTime = 0.1f;
|
||
|
float StillnessConfidenceRate = 1.f;
|
||
|
|
||
|
float StillnessGyroDelta = -1.f;
|
||
|
float StillnessAccelDelta = -1.f;
|
||
|
|
||
|
float SensorFusionCalibrationSmoothingStrength = 2.f;
|
||
|
float SensorFusionAngularAccelerationThreshold = 20.f;
|
||
|
float SensorFusionCalibrationEaseInTime = 3.f;
|
||
|
float SensorFusionCalibrationHalfTime = 0.1f;
|
||
|
float SensorFusionConfidenceRate = 1.f;
|
||
|
|
||
|
float GravityCorrectionShakinessMaxThreshold = 0.4f;
|
||
|
float GravityCorrectionShakinessMinThreshold = 0.01f;
|
||
|
|
||
|
float GravityCorrectionStillSpeed = 1.f;
|
||
|
float GravityCorrectionShakySpeed = 0.1f;
|
||
|
|
||
|
float GravityCorrectionGyroFactor = 0.1f;
|
||
|
float GravityCorrectionGyroMinThreshold = 0.05f;
|
||
|
float GravityCorrectionGyroMaxThreshold = 0.25f;
|
||
|
|
||
|
float GravityCorrectionMinimumSpeed = 0.01f;
|
||
|
};
|
||
|
|
||
|
class GamepadMotion
|
||
|
{
|
||
|
public:
|
||
|
GamepadMotion();
|
||
|
|
||
|
void Reset();
|
||
|
|
||
|
void ProcessMotion(float gyroX, float gyroY, float gyroZ,
|
||
|
float accelX, float accelY, float accelZ, float deltaTime);
|
||
|
|
||
|
// reading the current state
|
||
|
void GetCalibratedGyro(float& x, float& y, float& z);
|
||
|
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);
|
||
|
static void CalculatePlayerSpaceGyro(float& x, float& y, const float gyroX, const float gyroY, const float gyroZ, const float gravX, const float gravY, const float gravZ, const float yawRelaxFactor = 1.41f);
|
||
|
void GetWorldSpaceGyro(float& x, float& y, const float sideReductionThreshold = 0.125f);
|
||
|
static void CalculateWorldSpaceGyro(float& x, float& y, const float gyroX, const float gyroY, const float gyroZ, const float gravX, const float gravY, const float gravZ, const float sideReductionThreshold = 0.125f);
|
||
|
|
||
|
// gyro calibration functions
|
||
|
void StartContinuousCalibration();
|
||
|
void PauseContinuousCalibration();
|
||
|
void ResetContinuousCalibration();
|
||
|
void GetCalibrationOffset(float& xOffset, float& yOffset, float& zOffset);
|
||
|
void SetCalibrationOffset(float xOffset, float yOffset, float zOffset, int weight);
|
||
|
float GetAutoCalibrationConfidence();
|
||
|
void SetAutoCalibrationConfidence(float newConfidence);
|
||
|
bool GetAutoCalibrationIsSteady();
|
||
|
|
||
|
GamepadMotionHelpers::CalibrationMode GetCalibrationMode();
|
||
|
void SetCalibrationMode(GamepadMotionHelpers::CalibrationMode calibrationMode);
|
||
|
|
||
|
void ResetMotion();
|
||
|
|
||
|
GamepadMotionSettings Settings;
|
||
|
|
||
|
private:
|
||
|
GamepadMotionHelpers::Vec Gyro;
|
||
|
GamepadMotionHelpers::Vec RawAccel;
|
||
|
GamepadMotionHelpers::Motion Motion;
|
||
|
GamepadMotionHelpers::GyroCalibration GyroCalibration;
|
||
|
GamepadMotionHelpers::AutoCalibration AutoCalibration;
|
||
|
GamepadMotionHelpers::CalibrationMode CurrentCalibrationMode;
|
||
|
|
||
|
bool IsCalibrating;
|
||
|
void PushSensorSamples(float gyroX, float gyroY, float gyroZ, float accelMagnitude);
|
||
|
void GetCalibratedSensor(float& gyroOffsetX, float& gyroOffsetY, float& gyroOffsetZ, float& accelMagnitude);
|
||
|
};
|
||
|
|
||
|
///////////// Everything below here are just implementation details /////////////
|
||
|
|
||
|
namespace GamepadMotionHelpers
|
||
|
{
|
||
|
inline Quat::Quat()
|
||
|
{
|
||
|
w = 1.0f;
|
||
|
x = 0.0f;
|
||
|
y = 0.0f;
|
||
|
z = 0.0f;
|
||
|
}
|
||
|
|
||
|
inline Quat::Quat(float inW, float inX, float inY, float inZ)
|
||
|
{
|
||
|
w = inW;
|
||
|
x = inX;
|
||
|
y = inY;
|
||
|
z = inZ;
|
||
|
}
|
||
|
|
||
|
inline static Quat AngleAxis(float inAngle, float inX, float inY, float inZ)
|
||
|
{
|
||
|
const float sinHalfAngle = sinf(inAngle * 0.5f);
|
||
|
Vec inAxis = Vec(inX, inY, inZ);
|
||
|
inAxis.Normalize();
|
||
|
inAxis *= sinHalfAngle;
|
||
|
Quat result = Quat(cosf(inAngle * 0.5f), inAxis.x, inAxis.y, inAxis.z);
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
inline void Quat::Set(float inW, float inX, float inY, float inZ)
|
||
|
{
|
||
|
w = inW;
|
||
|
x = inX;
|
||
|
y = inY;
|
||
|
z = inZ;
|
||
|
}
|
||
|
|
||
|
inline Quat& Quat::operator*=(const Quat& rhs)
|
||
|
{
|
||
|
Set(w * rhs.w - x * rhs.x - y * rhs.y - z * rhs.z,
|
||
|
w * rhs.x + x * rhs.w + y * rhs.z - z * rhs.y,
|
||
|
w * rhs.y - x * rhs.z + y * rhs.w + z * rhs.x,
|
||
|
w * rhs.z + x * rhs.y - y * rhs.x + z * rhs.w);
|
||
|
return *this;
|
||
|
}
|
||
|
|
||
|
inline Quat operator*(Quat lhs, const Quat& rhs)
|
||
|
{
|
||
|
lhs *= rhs;
|
||
|
return lhs;
|
||
|
}
|
||
|
|
||
|
inline void Quat::Normalize()
|
||
|
{
|
||
|
const float length = sqrtf(w * w + x * x + y * y + z * z);
|
||
|
const float fixFactor = 1.0f / length;
|
||
|
|
||
|
w *= fixFactor;
|
||
|
x *= fixFactor;
|
||
|
y *= fixFactor;
|
||
|
z *= fixFactor;
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
inline Quat Quat::Normalized() const
|
||
|
{
|
||
|
Quat result = *this;
|
||
|
result.Normalize();
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
inline void Quat::Invert()
|
||
|
{
|
||
|
x = -x;
|
||
|
y = -y;
|
||
|
z = -z;
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
inline Quat Quat::Inverse() const
|
||
|
{
|
||
|
Quat result = *this;
|
||
|
result.Invert();
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
inline Vec::Vec()
|
||
|
{
|
||
|
x = 0.0f;
|
||
|
y = 0.0f;
|
||
|
z = 0.0f;
|
||
|
}
|
||
|
|
||
|
inline Vec::Vec(float inValue)
|
||
|
{
|
||
|
x = inValue;
|
||
|
y = inValue;
|
||
|
z = inValue;
|
||
|
}
|
||
|
|
||
|
inline Vec::Vec(float inX, float inY, float inZ)
|
||
|
{
|
||
|
x = inX;
|
||
|
y = inY;
|
||
|
z = inZ;
|
||
|
}
|
||
|
|
||
|
inline void Vec::Set(float inX, float inY, float inZ)
|
||
|
{
|
||
|
x = inX;
|
||
|
y = inY;
|
||
|
z = inZ;
|
||
|
}
|
||
|
|
||
|
inline float Vec::Length() const
|
||
|
{
|
||
|
return sqrtf(x * x + y * y + z * z);
|
||
|
}
|
||
|
|
||
|
inline float Vec::LengthSquared() const
|
||
|
{
|
||
|
return x * x + y * y + z * z;
|
||
|
}
|
||
|
|
||
|
inline void Vec::Normalize()
|
||
|
{
|
||
|
const float length = Length();
|
||
|
if (length == 0.0)
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
const float fixFactor = 1.0f / length;
|
||
|
|
||
|
x *= fixFactor;
|
||
|
y *= fixFactor;
|
||
|
z *= fixFactor;
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
inline Vec Vec::Normalized() const
|
||
|
{
|
||
|
Vec result = *this;
|
||
|
result.Normalize();
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
inline Vec& Vec::operator+=(const Vec& rhs)
|
||
|
{
|
||
|
Set(x + rhs.x, y + rhs.y, z + rhs.z);
|
||
|
return *this;
|
||
|
}
|
||
|
|
||
|
inline Vec operator+(Vec lhs, const Vec& rhs)
|
||
|
{
|
||
|
lhs += rhs;
|
||
|
return lhs;
|
||
|
}
|
||
|
|
||
|
inline Vec& Vec::operator-=(const Vec& rhs)
|
||
|
{
|
||
|
Set(x - rhs.x, y - rhs.y, z - rhs.z);
|
||
|
return *this;
|
||
|
}
|
||
|
|
||
|
inline Vec operator-(Vec lhs, const Vec& rhs)
|
||
|
{
|
||
|
lhs -= rhs;
|
||
|
return lhs;
|
||
|
}
|
||
|
|
||
|
inline Vec& Vec::operator*=(const float rhs)
|
||
|
{
|
||
|
Set(x * rhs, y * rhs, z * rhs);
|
||
|
return *this;
|
||
|
}
|
||
|
|
||
|
inline Vec operator*(Vec lhs, const float rhs)
|
||
|
{
|
||
|
lhs *= rhs;
|
||
|
return lhs;
|
||
|
}
|
||
|
|
||
|
inline Vec& Vec::operator/=(const float rhs)
|
||
|
{
|
||
|
Set(x / rhs, y / rhs, z / rhs);
|
||
|
return *this;
|
||
|
}
|
||
|
|
||
|
inline Vec operator/(Vec lhs, const float rhs)
|
||
|
{
|
||
|
lhs /= rhs;
|
||
|
return lhs;
|
||
|
}
|
||
|
|
||
|
inline Vec& Vec::operator*=(const Quat& rhs)
|
||
|
{
|
||
|
Quat temp = rhs * Quat(0.0f, x, y, z) * rhs.Inverse();
|
||
|
Set(temp.x, temp.y, temp.z);
|
||
|
return *this;
|
||
|
}
|
||
|
|
||
|
inline Vec operator*(Vec lhs, const Quat& rhs)
|
||
|
{
|
||
|
lhs *= rhs;
|
||
|
return lhs;
|
||
|
}
|
||
|
|
||
|
inline Vec Vec::operator-() const
|
||
|
{
|
||
|
Vec result = Vec(-x, -y, -z);
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
inline float Vec::Dot(const Vec& other) const
|
||
|
{
|
||
|
return x * other.x + y * other.y + z * other.z;
|
||
|
}
|
||
|
|
||
|
inline Vec Vec::Cross(const Vec& other) const
|
||
|
{
|
||
|
return Vec(y * other.z - z * other.y,
|
||
|
z * other.x - x * other.z,
|
||
|
x * other.y - y * other.x);
|
||
|
}
|
||
|
|
||
|
inline Vec Vec::Min(const Vec& other) const
|
||
|
{
|
||
|
return Vec(x < other.x ? x : other.x,
|
||
|
y < other.y ? y : other.y,
|
||
|
z < other.z ? z : other.z);
|
||
|
}
|
||
|
|
||
|
inline Vec Vec::Max(const Vec& other) const
|
||
|
{
|
||
|
return Vec(x > other.x ? x : other.x,
|
||
|
y > other.y ? y : other.y,
|
||
|
z > other.z ? z : other.z);
|
||
|
}
|
||
|
|
||
|
inline Vec Vec::Abs() const
|
||
|
{
|
||
|
return Vec(x > 0 ? x : -x,
|
||
|
y > 0 ? y : -y,
|
||
|
z > 0 ? z : -z);
|
||
|
}
|
||
|
|
||
|
inline Vec Vec::Lerp(const Vec& other, float factor) const
|
||
|
{
|
||
|
return *this + (other - *this) * factor;
|
||
|
}
|
||
|
|
||
|
inline Vec Vec::Lerp(const Vec& other, const Vec& factor) const
|
||
|
{
|
||
|
return Vec(this->x + (other.x - this->x) * factor.x,
|
||
|
this->y + (other.y - this->y) * factor.y,
|
||
|
this->z + (other.z - this->z) * factor.z);
|
||
|
}
|
||
|
|
||
|
inline Motion::Motion()
|
||
|
{
|
||
|
Reset();
|
||
|
}
|
||
|
|
||
|
inline void Motion::Reset()
|
||
|
{
|
||
|
Quaternion.Set(1.f, 0.f, 0.f, 0.f);
|
||
|
Accel.Set(0.f, 0.f, 0.f);
|
||
|
Grav.Set(0.f, 0.f, 0.f);
|
||
|
SmoothAccel.Set(0.f, 0.f, 0.f);
|
||
|
Shakiness = 0.f;
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// The gyro inputs should be calibrated degrees per second but have no other processing. Acceleration is in G units (1 = approx. 9.8m/s^2)
|
||
|
/// </summary>
|
||
|
inline void Motion::Update(float inGyroX, float inGyroY, float inGyroZ, float inAccelX, float inAccelY, float inAccelZ, float gravityLength, float deltaTime)
|
||
|
{
|
||
|
if (!Settings)
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// get settings
|
||
|
const float gravityCorrectionShakinessMinThreshold = Settings->GravityCorrectionShakinessMinThreshold;
|
||
|
const float gravityCorrectionShakinessMaxThreshold = Settings->GravityCorrectionShakinessMaxThreshold;
|
||
|
const float gravityCorrectionStillSpeed = Settings->GravityCorrectionStillSpeed;
|
||
|
const float gravityCorrectionShakySpeed = Settings->GravityCorrectionShakySpeed;
|
||
|
const float gravityCorrectionGyroFactor = Settings->GravityCorrectionGyroFactor;
|
||
|
const float gravityCorrectionGyroMinThreshold = Settings->GravityCorrectionGyroMinThreshold;
|
||
|
const float gravityCorrectionGyroMaxThreshold = Settings->GravityCorrectionGyroMaxThreshold;
|
||
|
const float gravityCorrectionMinimumSpeed = Settings->GravityCorrectionMinimumSpeed;
|
||
|
|
||
|
const Vec axis = Vec(inGyroX, inGyroY, inGyroZ);
|
||
|
const Vec accel = Vec(inAccelX, inAccelY, inAccelZ);
|
||
|
const float angleSpeed = axis.Length() * (float)M_PI / 180.0f;
|
||
|
const float angle = angleSpeed * deltaTime;
|
||
|
|
||
|
// rotate
|
||
|
Quat rotation = AngleAxis(angle, axis.x, axis.y, axis.z);
|
||
|
Quaternion *= rotation; // do it this way because it's a local rotation, not global
|
||
|
|
||
|
//printf("Quat: %.4f %.4f %.4f %.4f\n",
|
||
|
// Quaternion.w, Quaternion.x, Quaternion.y, Quaternion.z);
|
||
|
float accelMagnitude = accel.Length();
|
||
|
if (accelMagnitude > 0.0f)
|
||
|
{
|
||
|
const Vec accelNorm = accel / accelMagnitude;
|
||
|
// account for rotation when tracking smoothed acceleration
|
||
|
SmoothAccel *= rotation.Inverse();
|
||
|
//printf("Absolute Accel: %.4f %.4f %.4f\n",
|
||
|
// absoluteAccel.x, absoluteAccel.y, absoluteAccel.z);
|
||
|
const float smoothFactor = ShortSteadinessHalfTime <= 0.f ? 0.f : exp2f(-deltaTime / ShortSteadinessHalfTime);
|
||
|
Shakiness *= smoothFactor;
|
||
|
Shakiness = std::max(Shakiness, (accel - SmoothAccel).Length());
|
||
|
SmoothAccel = accel.Lerp(SmoothAccel, smoothFactor);
|
||
|
|
||
|
//printf("Shakiness: %.4f\n", Shakiness);
|
||
|
|
||
|
// update grav by rotation
|
||
|
Grav *= rotation.Inverse();
|
||
|
// we want to close the gap between grav and raw acceleration. What's the difference
|
||
|
const Vec gravToAccel = (accelNorm * -gravityLength) - Grav;
|
||
|
const Vec gravToAccelDir = gravToAccel.Normalized();
|
||
|
// adjustment rate
|
||
|
float gravCorrectionSpeed;
|
||
|
if (gravityCorrectionShakinessMinThreshold < gravityCorrectionShakinessMaxThreshold)
|
||
|
{
|
||
|
gravCorrectionSpeed = gravityCorrectionStillSpeed + (gravityCorrectionShakySpeed - gravityCorrectionStillSpeed) * std::clamp((Shakiness - gravityCorrectionShakinessMinThreshold) / (gravityCorrectionShakinessMaxThreshold - gravityCorrectionShakinessMinThreshold), 0.f, 1.f);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
gravCorrectionSpeed = Shakiness < gravityCorrectionShakinessMaxThreshold ? gravityCorrectionStillSpeed : gravityCorrectionShakySpeed;
|
||
|
}
|
||
|
// we also limit it to be no faster than a given proportion of the gyro rate, or the minimum gravity correction speed
|
||
|
const float gyroGravCorrectionLimit = std::max(angleSpeed * gravityCorrectionGyroFactor, gravityCorrectionMinimumSpeed);
|
||
|
if (gravCorrectionSpeed > gyroGravCorrectionLimit)
|
||
|
{
|
||
|
float closeEnoughFactor;
|
||
|
if (gravityCorrectionGyroMinThreshold < gravityCorrectionGyroMaxThreshold)
|
||
|
{
|
||
|
closeEnoughFactor = std::clamp((gravToAccel.Length() - gravityCorrectionGyroMinThreshold) / (gravityCorrectionGyroMaxThreshold - gravityCorrectionGyroMinThreshold), 0.f, 1.f);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
closeEnoughFactor = gravToAccel.Length() < gravityCorrectionGyroMaxThreshold ? 0.f : 1.f;
|
||
|
}
|
||
|
gravCorrectionSpeed = gyroGravCorrectionLimit + (gravCorrectionSpeed - gyroGravCorrectionLimit) * closeEnoughFactor;
|
||
|
}
|
||
|
const Vec gravToAccelDelta = gravToAccelDir * gravCorrectionSpeed * deltaTime;
|
||
|
if (gravToAccelDelta.LengthSquared() < gravToAccel.LengthSquared())
|
||
|
{
|
||
|
Grav += gravToAccelDelta;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
Grav = accelNorm * -gravityLength;
|
||
|
}
|
||
|
|
||
|
const Vec gravityDirection = Grav.Normalized() * Quaternion.Inverse(); // absolute gravity direction
|
||
|
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 = Quaternion * correctionQuat;
|
||
|
|
||
|
Accel = accel + Grav;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
Grav *= rotation.Inverse();
|
||
|
Accel = Grav;
|
||
|
}
|
||
|
Quaternion.Normalize();
|
||
|
}
|
||
|
|
||
|
inline void Motion::SetSettings(GamepadMotionSettings* settings)
|
||
|
{
|
||
|
Settings = settings;
|
||
|
}
|
||
|
|
||
|
inline SensorMinMaxWindow::SensorMinMaxWindow()
|
||
|
{
|
||
|
Reset(0.f);
|
||
|
}
|
||
|
|
||
|
inline void SensorMinMaxWindow::Reset(float remainder)
|
||
|
{
|
||
|
NumSamples = 0;
|
||
|
TimeSampled = remainder;
|
||
|
}
|
||
|
|
||
|
inline void SensorMinMaxWindow::AddSample(const Vec& inGyro, const Vec& inAccel, float deltaTime)
|
||
|
{
|
||
|
if (NumSamples == 0)
|
||
|
{
|
||
|
MaxGyro = inGyro;
|
||
|
MinGyro = inGyro;
|
||
|
MeanGyro = inGyro;
|
||
|
MaxAccel = inAccel;
|
||
|
MinAccel = inAccel;
|
||
|
MeanAccel = inAccel;
|
||
|
StartAccel = inAccel;
|
||
|
NumSamples = 1;
|
||
|
TimeSampled += deltaTime;
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
MaxGyro = MaxGyro.Max(inGyro);
|
||
|
MinGyro = MinGyro.Min(inGyro);
|
||
|
MaxAccel = MaxAccel.Max(inAccel);
|
||
|
MinAccel = MinAccel.Min(inAccel);
|
||
|
|
||
|
NumSamples++;
|
||
|
TimeSampled += deltaTime;
|
||
|
|
||
|
Vec delta = inGyro - MeanGyro;
|
||
|
MeanGyro += delta * (1.f / NumSamples);
|
||
|
delta = inAccel - MeanAccel;
|
||
|
MeanAccel += delta * (1.f / NumSamples);
|
||
|
}
|
||
|
|
||
|
inline Vec SensorMinMaxWindow::GetMidGyro()
|
||
|
{
|
||
|
return MeanGyro;
|
||
|
}
|
||
|
|
||
|
inline AutoCalibration::AutoCalibration()
|
||
|
{
|
||
|
CalibrationData = nullptr;
|
||
|
Reset();
|
||
|
}
|
||
|
|
||
|
inline void AutoCalibration::Reset()
|
||
|
{
|
||
|
MinMaxWindow.Reset(0.f);
|
||
|
Confidence = 0.f;
|
||
|
bIsSteady = false;
|
||
|
MinDeltaGyro = Vec(1.f);
|
||
|
MinDeltaAccel = Vec(0.25f);
|
||
|
RecalibrateThreshold = 1.f;
|
||
|
SensorFusionSkippedTime = 0.f;
|
||
|
TimeSteadySensorFusion = 0.f;
|
||
|
TimeSteadyStillness = 0.f;
|
||
|
}
|
||
|
|
||
|
inline bool AutoCalibration::AddSampleStillness(const Vec& inGyro, const Vec& inAccel, float deltaTime, bool doSensorFusion)
|
||
|
{
|
||
|
if (inGyro.x == 0.f && inGyro.y == 0.f && inGyro.z == 0.f &&
|
||
|
inAccel.x == 0.f && inAccel.y == 0.f && inAccel.z == 0.f)
|
||
|
{
|
||
|
// zeroes are almost certainly not valid inputs
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
if (!Settings)
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
if (!CalibrationData)
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// get settings
|
||
|
const int minStillnessSamples = Settings->MinStillnessSamples;
|
||
|
const float minStillnessCollectionTime = Settings->MinStillnessCollectionTime;
|
||
|
const float minStillnessCorrectionTime = Settings->MinStillnessCorrectionTime;
|
||
|
const float maxStillnessError = Settings->MaxStillnessError;
|
||
|
const float stillnessSampleDeteriorationRate = Settings->StillnessSampleDeteriorationRate;
|
||
|
const float stillnessErrorClimbRate = Settings->StillnessErrorClimbRate;
|
||
|
const float stillnessErrorDropOnRecalibrate = Settings->StillnessErrorDropOnRecalibrate;
|
||
|
const float stillnessCalibrationEaseInTime = Settings->StillnessCalibrationEaseInTime;
|
||
|
const float stillnessCalibrationHalfTime = Settings->StillnessCalibrationHalfTime * Confidence;
|
||
|
const float stillnessConfidenceRate = Settings->StillnessConfidenceRate;
|
||
|
const float stillnessGyroDelta = Settings->StillnessGyroDelta;
|
||
|
const float stillnessAccelDelta = Settings->StillnessAccelDelta;
|
||
|
|
||
|
MinMaxWindow.AddSample(inGyro, inAccel, deltaTime);
|
||
|
// get deltas
|
||
|
const Vec gyroDelta = MinMaxWindow.MaxGyro - MinMaxWindow.MinGyro;
|
||
|
const Vec accelDelta = MinMaxWindow.MaxAccel - MinMaxWindow.MinAccel;
|
||
|
|
||
|
bool calibrated = false;
|
||
|
bool isSteady = false;
|
||
|
const Vec climbThisTick = Vec(stillnessSampleDeteriorationRate * deltaTime);
|
||
|
if (stillnessGyroDelta < 0.f)
|
||
|
{
|
||
|
if (Confidence < 1.f)
|
||
|
{
|
||
|
MinDeltaGyro += climbThisTick;
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
MinDeltaGyro = Vec(stillnessGyroDelta);
|
||
|
}
|
||
|
if (stillnessAccelDelta < 0.f)
|
||
|
{
|
||
|
if (Confidence < 1.f)
|
||
|
{
|
||
|
MinDeltaAccel += climbThisTick;
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
MinDeltaAccel = Vec(stillnessAccelDelta);
|
||
|
}
|
||
|
|
||
|
//printf("Deltas: %.4f %.4f %.4f; %.4f %.4f %.4f\n",
|
||
|
// gyroDelta.x, gyroDelta.y, gyroDelta.z,
|
||
|
// accelDelta.x, accelDelta.y, accelDelta.z);
|
||
|
|
||
|
if (MinMaxWindow.NumSamples >= minStillnessSamples && MinMaxWindow.TimeSampled >= minStillnessCollectionTime)
|
||
|
{
|
||
|
MinDeltaGyro = MinDeltaGyro.Min(gyroDelta);
|
||
|
MinDeltaAccel = MinDeltaAccel.Min(accelDelta);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
RecalibrateThreshold = std::min(RecalibrateThreshold + stillnessErrorClimbRate * deltaTime, maxStillnessError);
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// check that all inputs are below appropriate thresholds to be considered "still"
|
||
|
if (gyroDelta.x <= MinDeltaGyro.x * RecalibrateThreshold &&
|
||
|
gyroDelta.y <= MinDeltaGyro.y * RecalibrateThreshold &&
|
||
|
gyroDelta.z <= MinDeltaGyro.z * RecalibrateThreshold &&
|
||
|
accelDelta.x <= MinDeltaAccel.x * RecalibrateThreshold &&
|
||
|
accelDelta.y <= MinDeltaAccel.y * RecalibrateThreshold &&
|
||
|
accelDelta.z <= MinDeltaAccel.z * RecalibrateThreshold)
|
||
|
{
|
||
|
if (MinMaxWindow.NumSamples >= minStillnessSamples && MinMaxWindow.TimeSampled >= minStillnessCorrectionTime)
|
||
|
{
|
||
|
/*if (TimeSteadyStillness == 0.f)
|
||
|
{
|
||
|
printf("Still!\n");
|
||
|
}/**/
|
||
|
|
||
|
TimeSteadyStillness = std::min(TimeSteadyStillness + deltaTime, stillnessCalibrationEaseInTime);
|
||
|
const float calibrationEaseIn = stillnessCalibrationEaseInTime <= 0.f ? 1.f : TimeSteadyStillness / stillnessCalibrationEaseInTime;
|
||
|
|
||
|
const Vec calibratedGyro = MinMaxWindow.GetMidGyro();
|
||
|
|
||
|
const Vec oldGyroBias = Vec(CalibrationData->X, CalibrationData->Y, CalibrationData->Z) / std::max((float)CalibrationData->NumSamples, 1.f);
|
||
|
const float stillnessLerpFactor = stillnessCalibrationHalfTime <= 0.f ? 0.f : exp2f(-calibrationEaseIn * deltaTime / stillnessCalibrationHalfTime);
|
||
|
Vec newGyroBias = calibratedGyro.Lerp(oldGyroBias, stillnessLerpFactor);
|
||
|
Confidence = std::min(Confidence + deltaTime * stillnessConfidenceRate, 1.f);
|
||
|
isSteady = true;
|
||
|
|
||
|
if (doSensorFusion)
|
||
|
{
|
||
|
const Vec previousNormal = MinMaxWindow.StartAccel.Normalized();
|
||
|
const Vec thisNormal = inAccel.Normalized();
|
||
|
Vec angularVelocity = thisNormal.Cross(previousNormal);
|
||
|
const float crossLength = angularVelocity.Length();
|
||
|
if (crossLength > 0.f)
|
||
|
{
|
||
|
const float thisDotPrev = std::clamp(thisNormal.Dot(previousNormal), -1.f, 1.f);
|
||
|
const float angleChange = acosf(thisDotPrev) * 180.0f / (float)M_PI;
|
||
|
const float anglePerSecond = angleChange / MinMaxWindow.TimeSampled;
|
||
|
angularVelocity *= anglePerSecond / crossLength;
|
||
|
}
|
||
|
|
||
|
Vec axisCalibrationStrength = thisNormal.Abs();
|
||
|
Vec sensorFusionBias = (calibratedGyro - angularVelocity).Lerp(oldGyroBias, stillnessLerpFactor);
|
||
|
if (axisCalibrationStrength.x <= 0.7f)
|
||
|
{
|
||
|
newGyroBias.x = sensorFusionBias.x;
|
||
|
}
|
||
|
if (axisCalibrationStrength.y <= 0.7f)
|
||
|
{
|
||
|
newGyroBias.y = sensorFusionBias.y;
|
||
|
}
|
||
|
if (axisCalibrationStrength.z <= 0.7f)
|
||
|
{
|
||
|
newGyroBias.z = sensorFusionBias.z;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
CalibrationData->X = newGyroBias.x;
|
||
|
CalibrationData->Y = newGyroBias.y;
|
||
|
CalibrationData->Z = newGyroBias.z;
|
||
|
|
||
|
CalibrationData->AccelMagnitude = MinMaxWindow.MeanAccel.Length();
|
||
|
CalibrationData->NumSamples = 1;
|
||
|
|
||
|
calibrated = true;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
RecalibrateThreshold = std::min(RecalibrateThreshold + stillnessErrorClimbRate * deltaTime, maxStillnessError);
|
||
|
}
|
||
|
}
|
||
|
else if (TimeSteadyStillness > 0.f)
|
||
|
{
|
||
|
//printf("Moved!\n");
|
||
|
RecalibrateThreshold -= stillnessErrorDropOnRecalibrate;
|
||
|
if (RecalibrateThreshold < 1.f) RecalibrateThreshold = 1.f;
|
||
|
|
||
|
TimeSteadyStillness = 0.f;
|
||
|
MinMaxWindow.Reset(0.f);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
RecalibrateThreshold = std::min(RecalibrateThreshold + stillnessErrorClimbRate * deltaTime, maxStillnessError);
|
||
|
MinMaxWindow.Reset(0.f);
|
||
|
}
|
||
|
|
||
|
bIsSteady = isSteady;
|
||
|
return calibrated;
|
||
|
}
|
||
|
|
||
|
inline void AutoCalibration::NoSampleStillness()
|
||
|
{
|
||
|
MinMaxWindow.Reset(0.f);
|
||
|
}
|
||
|
|
||
|
inline bool AutoCalibration::AddSampleSensorFusion(const Vec& inGyro, const Vec& inAccel, float deltaTime)
|
||
|
{
|
||
|
if (deltaTime <= 0.f)
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
if (inGyro.x == 0.f && inGyro.y == 0.f && inGyro.z == 0.f &&
|
||
|
inAccel.x == 0.f && inAccel.y == 0.f && inAccel.z == 0.f)
|
||
|
{
|
||
|
// all zeroes are almost certainly not valid inputs
|
||
|
TimeSteadySensorFusion = 0.f;
|
||
|
SensorFusionSkippedTime = 0.f;
|
||
|
PreviousAccel = inAccel;
|
||
|
SmoothedPreviousAccel = inAccel;
|
||
|
SmoothedAngularVelocityGyro = GamepadMotionHelpers::Vec();
|
||
|
SmoothedAngularVelocityAccel = GamepadMotionHelpers::Vec();
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
if (PreviousAccel.x == 0.f && PreviousAccel.y == 0.f && PreviousAccel.z == 0.f)
|
||
|
{
|
||
|
TimeSteadySensorFusion = 0.f;
|
||
|
SensorFusionSkippedTime = 0.f;
|
||
|
PreviousAccel = inAccel;
|
||
|
SmoothedPreviousAccel = inAccel;
|
||
|
SmoothedAngularVelocityGyro = GamepadMotionHelpers::Vec();
|
||
|
SmoothedAngularVelocityAccel = GamepadMotionHelpers::Vec();
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// in case the controller state hasn't updated between samples
|
||
|
if (inAccel.x == PreviousAccel.x && inAccel.y == PreviousAccel.y && inAccel.z == PreviousAccel.z)
|
||
|
{
|
||
|
SensorFusionSkippedTime += deltaTime;
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
if (!Settings)
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// get settings
|
||
|
const float sensorFusionCalibrationSmoothingStrength = Settings->SensorFusionCalibrationSmoothingStrength;
|
||
|
const float sensorFusionAngularAccelerationThreshold = Settings->SensorFusionAngularAccelerationThreshold;
|
||
|
const float sensorFusionCalibrationEaseInTime = Settings->SensorFusionCalibrationEaseInTime;
|
||
|
const float sensorFusionCalibrationHalfTime = Settings->SensorFusionCalibrationHalfTime * Confidence;
|
||
|
const float sensorFusionConfidenceRate = Settings->SensorFusionConfidenceRate;
|
||
|
|
||
|
deltaTime += SensorFusionSkippedTime;
|
||
|
SensorFusionSkippedTime = 0.f;
|
||
|
bool calibrated = false;
|
||
|
bool isSteady = false;
|
||
|
|
||
|
// framerate independent lerp smoothing: https://www.gamasutra.com/blogs/ScottLembcke/20180404/316046/Improved_Lerp_Smoothing.php
|
||
|
const float smoothingLerpFactor = exp2f(-sensorFusionCalibrationSmoothingStrength * deltaTime);
|
||
|
// velocity from smoothed accel matches better if we also smooth gyro
|
||
|
const Vec previousGyro = SmoothedAngularVelocityGyro;
|
||
|
SmoothedAngularVelocityGyro = inGyro.Lerp(SmoothedAngularVelocityGyro, smoothingLerpFactor); // smooth what remains
|
||
|
const float gyroAccelerationMag = (SmoothedAngularVelocityGyro - previousGyro).Length() / deltaTime;
|
||
|
// get angle between old and new accel
|
||
|
const Vec previousNormal = SmoothedPreviousAccel.Normalized();
|
||
|
const Vec thisAccel = inAccel.Lerp(SmoothedPreviousAccel, smoothingLerpFactor);
|
||
|
const Vec thisNormal = thisAccel.Normalized();
|
||
|
Vec angularVelocity = thisNormal.Cross(previousNormal);
|
||
|
const float crossLength = angularVelocity.Length();
|
||
|
if (crossLength > 0.f)
|
||
|
{
|
||
|
const float thisDotPrev = std::clamp(thisNormal.Dot(previousNormal), -1.f, 1.f);
|
||
|
const float angleChange = acosf(thisDotPrev) * 180.0f / (float)M_PI;
|
||
|
const float anglePerSecond = angleChange / deltaTime;
|
||
|
angularVelocity *= anglePerSecond / crossLength;
|
||
|
}
|
||
|
SmoothedAngularVelocityAccel = angularVelocity;
|
||
|
|
||
|
// apply corrections
|
||
|
if (gyroAccelerationMag > sensorFusionAngularAccelerationThreshold || CalibrationData == nullptr)
|
||
|
{
|
||
|
/*if (TimeSteadySensorFusion > 0.f)
|
||
|
{
|
||
|
printf("Shaken!\n");
|
||
|
}/**/
|
||
|
TimeSteadySensorFusion = 0.f;
|
||
|
//printf("No calibration due to acceleration of %.4f\n", gyroAccelerationMag);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
/*if (TimeSteadySensorFusion == 0.f)
|
||
|
{
|
||
|
printf("Steady!\n");
|
||
|
}/**/
|
||
|
|
||
|
TimeSteadySensorFusion = std::min(TimeSteadySensorFusion + deltaTime, sensorFusionCalibrationEaseInTime);
|
||
|
const float calibrationEaseIn = sensorFusionCalibrationEaseInTime <= 0.f ? 1.f : TimeSteadySensorFusion / sensorFusionCalibrationEaseInTime;
|
||
|
const Vec oldGyroBias = Vec(CalibrationData->X, CalibrationData->Y, CalibrationData->Z) / std::max((float)CalibrationData->NumSamples, 1.f);
|
||
|
// recalibrate over time proportional to the difference between the calculated bias and the current assumed bias
|
||
|
const float sensorFusionLerpFactor = sensorFusionCalibrationHalfTime <= 0.f ? 0.f : exp2f(-calibrationEaseIn * deltaTime / sensorFusionCalibrationHalfTime);
|
||
|
Vec newGyroBias = (SmoothedAngularVelocityGyro - SmoothedAngularVelocityAccel).Lerp(oldGyroBias, sensorFusionLerpFactor);
|
||
|
Confidence = std::min(Confidence + deltaTime * sensorFusionConfidenceRate, 1.f);
|
||
|
isSteady = true;
|
||
|
// don't change bias in axes that can't be affected by the gravity direction
|
||
|
Vec axisCalibrationStrength = thisNormal.Abs();
|
||
|
if (axisCalibrationStrength.x > 0.7f)
|
||
|
{
|
||
|
axisCalibrationStrength.x = 1.f;
|
||
|
}
|
||
|
if (axisCalibrationStrength.y > 0.7f)
|
||
|
{
|
||
|
axisCalibrationStrength.y = 1.f;
|
||
|
}
|
||
|
if (axisCalibrationStrength.z > 0.7f)
|
||
|
{
|
||
|
axisCalibrationStrength.z = 1.f;
|
||
|
}
|
||
|
newGyroBias = newGyroBias.Lerp(oldGyroBias, axisCalibrationStrength.Min(Vec(1.f)));
|
||
|
|
||
|
CalibrationData->X = newGyroBias.x;
|
||
|
CalibrationData->Y = newGyroBias.y;
|
||
|
CalibrationData->Z = newGyroBias.z;
|
||
|
|
||
|
CalibrationData->AccelMagnitude = thisAccel.Length();
|
||
|
|
||
|
CalibrationData->NumSamples = 1;
|
||
|
|
||
|
calibrated = true;
|
||
|
|
||
|
//printf("Recalibrating at a strength of %.4f\n", calibrationEaseIn);
|
||
|
}
|
||
|
|
||
|
SmoothedPreviousAccel = thisAccel;
|
||
|
PreviousAccel = inAccel;
|
||
|
|
||
|
//printf("Gyro: %.4f, %.4f, %.4f | Accel: %.4f, %.4f, %.4f\n",
|
||
|
// SmoothedAngularVelocityGyro.x, SmoothedAngularVelocityGyro.y, SmoothedAngularVelocityGyro.z,
|
||
|
// SmoothedAngularVelocityAccel.x, SmoothedAngularVelocityAccel.y, SmoothedAngularVelocityAccel.z);
|
||
|
|
||
|
bIsSteady = isSteady;
|
||
|
|
||
|
return calibrated;
|
||
|
}
|
||
|
|
||
|
inline void AutoCalibration::NoSampleSensorFusion()
|
||
|
{
|
||
|
TimeSteadySensorFusion = 0.f;
|
||
|
SensorFusionSkippedTime = 0.f;
|
||
|
PreviousAccel = GamepadMotionHelpers::Vec();
|
||
|
SmoothedPreviousAccel = GamepadMotionHelpers::Vec();
|
||
|
SmoothedAngularVelocityGyro = GamepadMotionHelpers::Vec();
|
||
|
SmoothedAngularVelocityAccel = GamepadMotionHelpers::Vec();
|
||
|
}
|
||
|
|
||
|
inline void AutoCalibration::SetCalibrationData(GyroCalibration* calibrationData)
|
||
|
{
|
||
|
CalibrationData = calibrationData;
|
||
|
}
|
||
|
|
||
|
inline void AutoCalibration::SetSettings(GamepadMotionSettings* settings)
|
||
|
{
|
||
|
Settings = settings;
|
||
|
}
|
||
|
|
||
|
} // namespace GamepadMotionHelpers
|
||
|
|
||
|
inline GamepadMotion::GamepadMotion()
|
||
|
{
|
||
|
IsCalibrating = false;
|
||
|
CurrentCalibrationMode = GamepadMotionHelpers::CalibrationMode::Manual;
|
||
|
Reset();
|
||
|
AutoCalibration.SetCalibrationData(&GyroCalibration);
|
||
|
AutoCalibration.SetSettings(&Settings);
|
||
|
Motion.SetSettings(&Settings);
|
||
|
}
|
||
|
|
||
|
inline void GamepadMotion::Reset()
|
||
|
{
|
||
|
GyroCalibration = {};
|
||
|
Gyro = {};
|
||
|
RawAccel = {};
|
||
|
Settings = GamepadMotionSettings();
|
||
|
Motion.Reset();
|
||
|
}
|
||
|
|
||
|
inline void GamepadMotion::ProcessMotion(float gyroX, float gyroY, float gyroZ,
|
||
|
float accelX, float accelY, float accelZ, float deltaTime)
|
||
|
{
|
||
|
if (gyroX == 0.f && gyroY == 0.f && gyroZ == 0.f &&
|
||
|
accelX == 0.f && accelY == 0.f && accelZ == 0.f)
|
||
|
{
|
||
|
// all zeroes are almost certainly not valid inputs
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
float accelMagnitude = sqrtf(accelX * accelX + accelY * accelY + accelZ * accelZ);
|
||
|
|
||
|
if (IsCalibrating)
|
||
|
{
|
||
|
// manual calibration
|
||
|
PushSensorSamples(gyroX, gyroY, gyroZ, accelMagnitude);
|
||
|
AutoCalibration.NoSampleSensorFusion();
|
||
|
AutoCalibration.NoSampleStillness();
|
||
|
}
|
||
|
else if (CurrentCalibrationMode & GamepadMotionHelpers::CalibrationMode::Stillness)
|
||
|
{
|
||
|
AutoCalibration.AddSampleStillness(GamepadMotionHelpers::Vec(gyroX, gyroY, gyroZ), GamepadMotionHelpers::Vec(accelX, accelY, accelZ), deltaTime, CurrentCalibrationMode & GamepadMotionHelpers::CalibrationMode::SensorFusion);
|
||
|
AutoCalibration.NoSampleSensorFusion();
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
AutoCalibration.NoSampleStillness();
|
||
|
if (CurrentCalibrationMode & GamepadMotionHelpers::CalibrationMode::SensorFusion)
|
||
|
{
|
||
|
AutoCalibration.AddSampleSensorFusion(GamepadMotionHelpers::Vec(gyroX, gyroY, gyroZ), GamepadMotionHelpers::Vec(accelX, accelY, accelZ), deltaTime);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
AutoCalibration.NoSampleSensorFusion();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
float gyroOffsetX, gyroOffsetY, gyroOffsetZ;
|
||
|
GetCalibratedSensor(gyroOffsetX, gyroOffsetY, gyroOffsetZ, accelMagnitude);
|
||
|
|
||
|
gyroX -= gyroOffsetX;
|
||
|
gyroY -= gyroOffsetY;
|
||
|
gyroZ -= gyroOffsetZ;
|
||
|
|
||
|
Motion.Update(gyroX, gyroY, gyroZ, accelX, accelY, accelZ, accelMagnitude, deltaTime);
|
||
|
|
||
|
Gyro.x = gyroX;
|
||
|
Gyro.y = gyroY;
|
||
|
Gyro.z = gyroZ;
|
||
|
RawAccel.x = accelX;
|
||
|
RawAccel.y = accelY;
|
||
|
RawAccel.z = accelZ;
|
||
|
}
|
||
|
|
||
|
// reading the current state
|
||
|
inline void GamepadMotion::GetCalibratedGyro(float& x, float& y, float& z)
|
||
|
{
|
||
|
x = Gyro.x;
|
||
|
y = Gyro.y;
|
||
|
z = Gyro.z;
|
||
|
}
|
||
|
|
||
|
inline void GamepadMotion::GetGravity(float& x, float& y, float& z)
|
||
|
{
|
||
|
x = Motion.Grav.x;
|
||
|
y = Motion.Grav.y;
|
||
|
z = Motion.Grav.z;
|
||
|
}
|
||
|
|
||
|
inline void GamepadMotion::GetProcessedAcceleration(float& x, float& y, float& z)
|
||
|
{
|
||
|
x = Motion.Accel.x;
|
||
|
y = Motion.Accel.y;
|
||
|
z = Motion.Accel.z;
|
||
|
}
|
||
|
|
||
|
inline void GamepadMotion::GetOrientation(float& w, float& x, float& y, float& z)
|
||
|
{
|
||
|
w = Motion.Quaternion.w;
|
||
|
x = Motion.Quaternion.x;
|
||
|
y = Motion.Quaternion.y;
|
||
|
z = Motion.Quaternion.z;
|
||
|
}
|
||
|
|
||
|
inline void GamepadMotion::GetPlayerSpaceGyro(float& x, float& y, const float yawRelaxFactor)
|
||
|
{
|
||
|
CalculatePlayerSpaceGyro(x, y, Gyro.x, Gyro.y, Gyro.z, Motion.Grav.x, Motion.Grav.y, Motion.Grav.z, yawRelaxFactor);
|
||
|
}
|
||
|
|
||
|
inline void GamepadMotion::CalculatePlayerSpaceGyro(float& x, float& y, const float gyroX, const float gyroY, const float gyroZ, const float gravX, const float gravY, const float gravZ, 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 = -(gravY * gyroY + gravZ * gyroZ);
|
||
|
const float worldYawSign = worldYaw < 0.f ? -1.f : 1.f;
|
||
|
y = worldYawSign * std::min(std::abs(worldYaw) * yawRelaxFactor, sqrtf(gyroY * gyroY + gyroZ * gyroZ));
|
||
|
x = gyroX;
|
||
|
}
|
||
|
|
||
|
inline void GamepadMotion::GetWorldSpaceGyro(float& x, float& y, const float sideReductionThreshold)
|
||
|
{
|
||
|
CalculateWorldSpaceGyro(x, y, Gyro.x, Gyro.y, Gyro.z, Motion.Grav.x, Motion.Grav.y, Motion.Grav.z, sideReductionThreshold);
|
||
|
}
|
||
|
|
||
|
inline void GamepadMotion::CalculateWorldSpaceGyro(float& x, float& y, const float gyroX, const float gyroY, const float gyroZ, const float gravX, const float gravY, const float gravZ, 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 = -gravX * gyroX - gravY * gyroY - gravZ * gyroZ;
|
||
|
// project local pitch axis (X) onto gravity plane
|
||
|
const float gravDotPitchAxis = gravX;
|
||
|
GamepadMotionHelpers::Vec pitchAxis(1.f - gravX * gravDotPitchAxis,
|
||
|
-gravY * gravDotPitchAxis,
|
||
|
-gravZ * 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(gravY);
|
||
|
const float upness = std::abs(gravZ);
|
||
|
const float sideReduction = sideReductionThreshold <= 0.f ? 1.f : std::clamp((std::max(flatness, upness) - sideReductionThreshold) / sideReductionThreshold, 0.f, 1.f);
|
||
|
|
||
|
x = sideReduction * pitchAxis.Dot(GamepadMotionHelpers::Vec(gyroX, gyroY, gyroZ));
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
x = 0.f;
|
||
|
}
|
||
|
|
||
|
y = worldYaw;
|
||
|
}
|
||
|
|
||
|
// gyro calibration functions
|
||
|
inline void GamepadMotion::StartContinuousCalibration()
|
||
|
{
|
||
|
IsCalibrating = true;
|
||
|
}
|
||
|
|
||
|
inline void GamepadMotion::PauseContinuousCalibration()
|
||
|
{
|
||
|
IsCalibrating = false;
|
||
|
}
|
||
|
|
||
|
inline void GamepadMotion::ResetContinuousCalibration()
|
||
|
{
|
||
|
GyroCalibration = {};
|
||
|
AutoCalibration.Reset();
|
||
|
}
|
||
|
|
||
|
inline void GamepadMotion::GetCalibrationOffset(float& xOffset, float& yOffset, float& zOffset)
|
||
|
{
|
||
|
float accelMagnitude;
|
||
|
GetCalibratedSensor(xOffset, yOffset, zOffset, accelMagnitude);
|
||
|
}
|
||
|
|
||
|
inline void GamepadMotion::SetCalibrationOffset(float xOffset, float yOffset, float zOffset, int weight)
|
||
|
{
|
||
|
if (GyroCalibration.NumSamples > 1)
|
||
|
{
|
||
|
GyroCalibration.AccelMagnitude *= ((float)weight) / GyroCalibration.NumSamples;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
GyroCalibration.AccelMagnitude = (float)weight;
|
||
|
}
|
||
|
|
||
|
GyroCalibration.NumSamples = weight;
|
||
|
GyroCalibration.X = xOffset * weight;
|
||
|
GyroCalibration.Y = yOffset * weight;
|
||
|
GyroCalibration.Z = zOffset * weight;
|
||
|
}
|
||
|
|
||
|
inline float GamepadMotion::GetAutoCalibrationConfidence()
|
||
|
{
|
||
|
return AutoCalibration.Confidence;
|
||
|
}
|
||
|
|
||
|
inline void GamepadMotion::SetAutoCalibrationConfidence(float newConfidence)
|
||
|
{
|
||
|
AutoCalibration.Confidence = newConfidence;
|
||
|
}
|
||
|
|
||
|
inline bool GamepadMotion::GetAutoCalibrationIsSteady()
|
||
|
{
|
||
|
return AutoCalibration.IsSteady();
|
||
|
}
|
||
|
|
||
|
inline GamepadMotionHelpers::CalibrationMode GamepadMotion::GetCalibrationMode()
|
||
|
{
|
||
|
return CurrentCalibrationMode;
|
||
|
}
|
||
|
|
||
|
inline void GamepadMotion::SetCalibrationMode(GamepadMotionHelpers::CalibrationMode calibrationMode)
|
||
|
{
|
||
|
CurrentCalibrationMode = calibrationMode;
|
||
|
}
|
||
|
|
||
|
inline void GamepadMotion::ResetMotion()
|
||
|
{
|
||
|
Motion.Reset();
|
||
|
}
|
||
|
|
||
|
// Private Methods
|
||
|
|
||
|
inline void GamepadMotion::PushSensorSamples(float gyroX, float gyroY, float gyroZ, float accelMagnitude)
|
||
|
{
|
||
|
// accumulate
|
||
|
GyroCalibration.NumSamples++;
|
||
|
GyroCalibration.X += gyroX;
|
||
|
GyroCalibration.Y += gyroY;
|
||
|
GyroCalibration.Z += gyroZ;
|
||
|
GyroCalibration.AccelMagnitude += accelMagnitude;
|
||
|
}
|
||
|
|
||
|
inline void GamepadMotion::GetCalibratedSensor(float& gyroOffsetX, float& gyroOffsetY, float& gyroOffsetZ, float& accelMagnitude)
|
||
|
{
|
||
|
if (GyroCalibration.NumSamples <= 0)
|
||
|
{
|
||
|
gyroOffsetX = 0.f;
|
||
|
gyroOffsetY = 0.f;
|
||
|
gyroOffsetZ = 0.f;
|
||
|
accelMagnitude = 1.f;
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const float inverseSamples = 1.f / GyroCalibration.NumSamples;
|
||
|
gyroOffsetX = GyroCalibration.X * inverseSamples;
|
||
|
gyroOffsetY = GyroCalibration.Y * inverseSamples;
|
||
|
gyroOffsetZ = GyroCalibration.Z * inverseSamples;
|
||
|
accelMagnitude = GyroCalibration.AccelMagnitude * inverseSamples;
|
||
|
}
|