Path: blob/master/thirdparty/gamepadmotionhelpers/GamepadMotion.hpp
21425 views
// Copyright (c) 2020-2023 Julian "Jibb" Smart1// Released under the MIT license. See https://github.com/JibbSmart/GamepadMotionHelpers/blob/main/LICENSE for more info2// Version 934#pragma once56#define _USE_MATH_DEFINES7#include <math.h>8#include <algorithm> // std::min, std::max and std::clamp910// You don't need to look at these. These will just be used internally by the GamepadMotion class declared below.11// You can ignore anything in namespace GamepadMotionHelpers.12class GamepadMotionSettings;13class GamepadMotion;1415namespace GamepadMotionHelpers16{17struct GyroCalibration18{19float X;20float Y;21float Z;22float AccelMagnitude;23int NumSamples;24};2526struct Quat27{28float w;29float x;30float y;31float z;3233Quat();34Quat(float inW, float inX, float inY, float inZ);35void Set(float inW, float inX, float inY, float inZ);36Quat& operator*=(const Quat& rhs);37friend Quat operator*(Quat lhs, const Quat& rhs);38void Normalize();39Quat Normalized() const;40void Invert();41Quat Inverse() const;42};4344struct Vec45{46float x;47float y;48float z;4950Vec();51Vec(float inValue);52Vec(float inX, float inY, float inZ);53void Set(float inX, float inY, float inZ);54float Length() const;55float LengthSquared() const;56void Normalize();57Vec Normalized() const;58float Dot(const Vec& other) const;59Vec Cross(const Vec& other) const;60Vec Min(const Vec& other) const;61Vec Max(const Vec& other) const;62Vec Abs() const;63Vec Lerp(const Vec& other, float factor) const;64Vec Lerp(const Vec& other, const Vec& factor) const;65Vec& operator+=(const Vec& rhs);66friend Vec operator+(Vec lhs, const Vec& rhs);67Vec& operator-=(const Vec& rhs);68friend Vec operator-(Vec lhs, const Vec& rhs);69Vec& operator*=(const float rhs);70friend Vec operator*(Vec lhs, const float rhs);71Vec& operator/=(const float rhs);72friend Vec operator/(Vec lhs, const float rhs);73Vec& operator*=(const Quat& rhs);74friend Vec operator*(Vec lhs, const Quat& rhs);75Vec operator-() const;76};7778struct SensorMinMaxWindow79{80Vec MinGyro;81Vec MaxGyro;82Vec MeanGyro;83Vec MinAccel;84Vec MaxAccel;85Vec MeanAccel;86Vec StartAccel;87int NumSamples = 0;88float TimeSampled = 0.f;8990SensorMinMaxWindow();91void Reset(float remainder);92void AddSample(const Vec& inGyro, const Vec& inAccel, float deltaTime);93Vec GetMidGyro();94};9596struct AutoCalibration97{98SensorMinMaxWindow MinMaxWindow;99Vec SmoothedAngularVelocityGyro;100Vec SmoothedAngularVelocityAccel;101Vec SmoothedPreviousAccel;102Vec PreviousAccel;103104AutoCalibration();105void Reset();106bool AddSampleStillness(const Vec& inGyro, const Vec& inAccel, float deltaTime, bool doSensorFusion);107void NoSampleStillness();108bool AddSampleSensorFusion(const Vec& inGyro, const Vec& inAccel, float deltaTime);109void NoSampleSensorFusion();110void SetCalibrationData(GyroCalibration* calibrationData);111void SetSettings(GamepadMotionSettings* settings);112113float Confidence = 0.f;114bool IsSteady() { return bIsSteady; }115116private:117Vec MinDeltaGyro = Vec(1.f);118Vec MinDeltaAccel = Vec(0.25f);119float RecalibrateThreshold = 1.f;120float SensorFusionSkippedTime = 0.f;121float TimeSteadySensorFusion = 0.f;122float TimeSteadyStillness = 0.f;123bool bIsSteady = false;124125GyroCalibration* CalibrationData;126GamepadMotionSettings* Settings;127};128129struct Motion130{131Quat Quaternion;132Vec Accel;133Vec Grav;134135Vec SmoothAccel = Vec();136float Shakiness = 0.f;137const float ShortSteadinessHalfTime = 0.25f;138const float LongSteadinessHalfTime = 1.f;139140Motion();141void Reset();142void Update(float inGyroX, float inGyroY, float inGyroZ, float inAccelX, float inAccelY, float inAccelZ, float gravityLength, float deltaTime);143void SetSettings(GamepadMotionSettings* settings);144145private:146GamepadMotionSettings* Settings;147};148149enum CalibrationMode150{151Manual = 0,152Stillness = 1,153SensorFusion = 2,154};155156// https://stackoverflow.com/a/1448478/1130520157inline CalibrationMode operator|(CalibrationMode a, CalibrationMode b)158{159return static_cast<CalibrationMode>(static_cast<int>(a) | static_cast<int>(b));160}161162inline CalibrationMode operator&(CalibrationMode a, CalibrationMode b)163{164return static_cast<CalibrationMode>(static_cast<int>(a) & static_cast<int>(b));165}166167inline CalibrationMode operator~(CalibrationMode a)168{169return static_cast<CalibrationMode>(~static_cast<int>(a));170}171172// https://stackoverflow.com/a/23152590/1130520173inline CalibrationMode& operator|=(CalibrationMode& a, CalibrationMode b)174{175return (CalibrationMode&)((int&)(a) |= static_cast<int>(b));176}177178inline CalibrationMode& operator&=(CalibrationMode& a, CalibrationMode b)179{180return (CalibrationMode&)((int&)(a) &= static_cast<int>(b));181}182}183184// Note that I'm using a Y-up coordinate system. This is to follow the convention set by the motion sensors in185// PlayStation controllers, which was what I was using when writing in this. But for the record, Z-up is186// better for most games (XY ground-plane in 3D games simplifies using 2D vectors in navigation, for example).187188// Gyro units should be degrees per second. Accelerometer should be g-force (approx. 9.8 m/s^2 = 1 g). If you're using189// radians per second, meters per second squared, etc, conversion should be simple.190191class GamepadMotionSettings192{193public:194int MinStillnessSamples = 10;195float MinStillnessCollectionTime = 0.5f;196float MinStillnessCorrectionTime = 2.f;197float MaxStillnessError = 2.f;198float StillnessSampleDeteriorationRate = 0.2f;199float StillnessErrorClimbRate = 0.1f;200float StillnessErrorDropOnRecalibrate = 0.1f;201float StillnessCalibrationEaseInTime = 3.f;202float StillnessCalibrationHalfTime = 0.1f;203float StillnessConfidenceRate = 1.f;204205float StillnessGyroDelta = -1.f;206float StillnessAccelDelta = -1.f;207208float SensorFusionCalibrationSmoothingStrength = 2.f;209float SensorFusionAngularAccelerationThreshold = 20.f;210float SensorFusionCalibrationEaseInTime = 3.f;211float SensorFusionCalibrationHalfTime = 0.1f;212float SensorFusionConfidenceRate = 1.f;213214float GravityCorrectionShakinessMaxThreshold = 0.4f;215float GravityCorrectionShakinessMinThreshold = 0.01f;216217float GravityCorrectionStillSpeed = 1.f;218float GravityCorrectionShakySpeed = 0.1f;219220float GravityCorrectionGyroFactor = 0.1f;221float GravityCorrectionGyroMinThreshold = 0.05f;222float GravityCorrectionGyroMaxThreshold = 0.25f;223224float GravityCorrectionMinimumSpeed = 0.01f;225};226227class GamepadMotion228{229public:230GamepadMotion();231232void Reset();233234void ProcessMotion(float gyroX, float gyroY, float gyroZ,235float accelX, float accelY, float accelZ, float deltaTime);236237// reading the current state238void GetCalibratedGyro(float& x, float& y, float& z);239void GetGravity(float& x, float& y, float& z);240void GetProcessedAcceleration(float& x, float& y, float& z);241void GetOrientation(float& w, float& x, float& y, float& z);242void GetPlayerSpaceGyro(float& x, float& y, const float yawRelaxFactor = 1.41f);243static 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);244void GetWorldSpaceGyro(float& x, float& y, const float sideReductionThreshold = 0.125f);245static 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);246247// gyro calibration functions248void StartContinuousCalibration();249void PauseContinuousCalibration();250void ResetContinuousCalibration();251void GetCalibrationOffset(float& xOffset, float& yOffset, float& zOffset);252void SetCalibrationOffset(float xOffset, float yOffset, float zOffset, int weight);253float GetAutoCalibrationConfidence();254void SetAutoCalibrationConfidence(float newConfidence);255bool GetAutoCalibrationIsSteady();256257GamepadMotionHelpers::CalibrationMode GetCalibrationMode();258void SetCalibrationMode(GamepadMotionHelpers::CalibrationMode calibrationMode);259260void ResetMotion();261262GamepadMotionSettings Settings;263264private:265GamepadMotionHelpers::Vec Gyro;266GamepadMotionHelpers::Vec RawAccel;267GamepadMotionHelpers::Motion Motion;268GamepadMotionHelpers::GyroCalibration GyroCalibration;269GamepadMotionHelpers::AutoCalibration AutoCalibration;270GamepadMotionHelpers::CalibrationMode CurrentCalibrationMode;271272bool IsCalibrating;273void PushSensorSamples(float gyroX, float gyroY, float gyroZ, float accelMagnitude);274void GetCalibratedSensor(float& gyroOffsetX, float& gyroOffsetY, float& gyroOffsetZ, float& accelMagnitude);275};276277///////////// Everything below here are just implementation details /////////////278279namespace GamepadMotionHelpers280{281inline Quat::Quat()282{283w = 1.0f;284x = 0.0f;285y = 0.0f;286z = 0.0f;287}288289inline Quat::Quat(float inW, float inX, float inY, float inZ)290{291w = inW;292x = inX;293y = inY;294z = inZ;295}296297inline static Quat AngleAxis(float inAngle, float inX, float inY, float inZ)298{299const float sinHalfAngle = sinf(inAngle * 0.5f);300Vec inAxis = Vec(inX, inY, inZ);301inAxis.Normalize();302inAxis *= sinHalfAngle;303Quat result = Quat(cosf(inAngle * 0.5f), inAxis.x, inAxis.y, inAxis.z);304return result;305}306307inline void Quat::Set(float inW, float inX, float inY, float inZ)308{309w = inW;310x = inX;311y = inY;312z = inZ;313}314315inline Quat& Quat::operator*=(const Quat& rhs)316{317Set(w * rhs.w - x * rhs.x - y * rhs.y - z * rhs.z,318w * rhs.x + x * rhs.w + y * rhs.z - z * rhs.y,319w * rhs.y - x * rhs.z + y * rhs.w + z * rhs.x,320w * rhs.z + x * rhs.y - y * rhs.x + z * rhs.w);321return *this;322}323324inline Quat operator*(Quat lhs, const Quat& rhs)325{326lhs *= rhs;327return lhs;328}329330inline void Quat::Normalize()331{332const float length = sqrtf(w * w + x * x + y * y + z * z);333const float fixFactor = 1.0f / length;334335w *= fixFactor;336x *= fixFactor;337y *= fixFactor;338z *= fixFactor;339340return;341}342343inline Quat Quat::Normalized() const344{345Quat result = *this;346result.Normalize();347return result;348}349350inline void Quat::Invert()351{352x = -x;353y = -y;354z = -z;355return;356}357358inline Quat Quat::Inverse() const359{360Quat result = *this;361result.Invert();362return result;363}364365inline Vec::Vec()366{367x = 0.0f;368y = 0.0f;369z = 0.0f;370}371372inline Vec::Vec(float inValue)373{374x = inValue;375y = inValue;376z = inValue;377}378379inline Vec::Vec(float inX, float inY, float inZ)380{381x = inX;382y = inY;383z = inZ;384}385386inline void Vec::Set(float inX, float inY, float inZ)387{388x = inX;389y = inY;390z = inZ;391}392393inline float Vec::Length() const394{395return sqrtf(x * x + y * y + z * z);396}397398inline float Vec::LengthSquared() const399{400return x * x + y * y + z * z;401}402403inline void Vec::Normalize()404{405const float length = Length();406if (length == 0.0)407{408return;409}410const float fixFactor = 1.0f / length;411412x *= fixFactor;413y *= fixFactor;414z *= fixFactor;415return;416}417418inline Vec Vec::Normalized() const419{420Vec result = *this;421result.Normalize();422return result;423}424425inline Vec& Vec::operator+=(const Vec& rhs)426{427Set(x + rhs.x, y + rhs.y, z + rhs.z);428return *this;429}430431inline Vec operator+(Vec lhs, const Vec& rhs)432{433lhs += rhs;434return lhs;435}436437inline Vec& Vec::operator-=(const Vec& rhs)438{439Set(x - rhs.x, y - rhs.y, z - rhs.z);440return *this;441}442443inline Vec operator-(Vec lhs, const Vec& rhs)444{445lhs -= rhs;446return lhs;447}448449inline Vec& Vec::operator*=(const float rhs)450{451Set(x * rhs, y * rhs, z * rhs);452return *this;453}454455inline Vec operator*(Vec lhs, const float rhs)456{457lhs *= rhs;458return lhs;459}460461inline Vec& Vec::operator/=(const float rhs)462{463Set(x / rhs, y / rhs, z / rhs);464return *this;465}466467inline Vec operator/(Vec lhs, const float rhs)468{469lhs /= rhs;470return lhs;471}472473inline Vec& Vec::operator*=(const Quat& rhs)474{475Quat temp = rhs * Quat(0.0f, x, y, z) * rhs.Inverse();476Set(temp.x, temp.y, temp.z);477return *this;478}479480inline Vec operator*(Vec lhs, const Quat& rhs)481{482lhs *= rhs;483return lhs;484}485486inline Vec Vec::operator-() const487{488Vec result = Vec(-x, -y, -z);489return result;490}491492inline float Vec::Dot(const Vec& other) const493{494return x * other.x + y * other.y + z * other.z;495}496497inline Vec Vec::Cross(const Vec& other) const498{499return Vec(y * other.z - z * other.y,500z * other.x - x * other.z,501x * other.y - y * other.x);502}503504inline Vec Vec::Min(const Vec& other) const505{506return Vec(x < other.x ? x : other.x,507y < other.y ? y : other.y,508z < other.z ? z : other.z);509}510511inline Vec Vec::Max(const Vec& other) const512{513return Vec(x > other.x ? x : other.x,514y > other.y ? y : other.y,515z > other.z ? z : other.z);516}517518inline Vec Vec::Abs() const519{520return Vec(x > 0 ? x : -x,521y > 0 ? y : -y,522z > 0 ? z : -z);523}524525inline Vec Vec::Lerp(const Vec& other, float factor) const526{527return *this + (other - *this) * factor;528}529530inline Vec Vec::Lerp(const Vec& other, const Vec& factor) const531{532return Vec(this->x + (other.x - this->x) * factor.x,533this->y + (other.y - this->y) * factor.y,534this->z + (other.z - this->z) * factor.z);535}536537inline Motion::Motion()538{539Reset();540}541542inline void Motion::Reset()543{544Quaternion.Set(1.f, 0.f, 0.f, 0.f);545Accel.Set(0.f, 0.f, 0.f);546Grav.Set(0.f, 0.f, 0.f);547SmoothAccel.Set(0.f, 0.f, 0.f);548Shakiness = 0.f;549}550551/// <summary>552/// 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)553/// </summary>554inline void Motion::Update(float inGyroX, float inGyroY, float inGyroZ, float inAccelX, float inAccelY, float inAccelZ, float gravityLength, float deltaTime)555{556if (!Settings)557{558return;559}560561// get settings562const float gravityCorrectionShakinessMinThreshold = Settings->GravityCorrectionShakinessMinThreshold;563const float gravityCorrectionShakinessMaxThreshold = Settings->GravityCorrectionShakinessMaxThreshold;564const float gravityCorrectionStillSpeed = Settings->GravityCorrectionStillSpeed;565const float gravityCorrectionShakySpeed = Settings->GravityCorrectionShakySpeed;566const float gravityCorrectionGyroFactor = Settings->GravityCorrectionGyroFactor;567const float gravityCorrectionGyroMinThreshold = Settings->GravityCorrectionGyroMinThreshold;568const float gravityCorrectionGyroMaxThreshold = Settings->GravityCorrectionGyroMaxThreshold;569const float gravityCorrectionMinimumSpeed = Settings->GravityCorrectionMinimumSpeed;570571const Vec axis = Vec(inGyroX, inGyroY, inGyroZ);572const Vec accel = Vec(inAccelX, inAccelY, inAccelZ);573const float angleSpeed = axis.Length() * (float)M_PI / 180.0f;574const float angle = angleSpeed * deltaTime;575576// rotate577Quat rotation = AngleAxis(angle, axis.x, axis.y, axis.z);578Quaternion *= rotation; // do it this way because it's a local rotation, not global579580//printf("Quat: %.4f %.4f %.4f %.4f\n",581// Quaternion.w, Quaternion.x, Quaternion.y, Quaternion.z);582float accelMagnitude = accel.Length();583if (accelMagnitude > 0.0f)584{585const Vec accelNorm = accel / accelMagnitude;586// account for rotation when tracking smoothed acceleration587SmoothAccel *= rotation.Inverse();588//printf("Absolute Accel: %.4f %.4f %.4f\n",589// absoluteAccel.x, absoluteAccel.y, absoluteAccel.z);590const float smoothFactor = ShortSteadinessHalfTime <= 0.f ? 0.f : exp2f(-deltaTime / ShortSteadinessHalfTime);591Shakiness *= smoothFactor;592Shakiness = std::max(Shakiness, (accel - SmoothAccel).Length());593SmoothAccel = accel.Lerp(SmoothAccel, smoothFactor);594595//printf("Shakiness: %.4f\n", Shakiness);596597// update grav by rotation598Grav *= rotation.Inverse();599// we want to close the gap between grav and raw acceleration. What's the difference600const Vec gravToAccel = (accelNorm * -gravityLength) - Grav;601const Vec gravToAccelDir = gravToAccel.Normalized();602// adjustment rate603float gravCorrectionSpeed;604if (gravityCorrectionShakinessMinThreshold < gravityCorrectionShakinessMaxThreshold)605{606gravCorrectionSpeed = gravityCorrectionStillSpeed + (gravityCorrectionShakySpeed - gravityCorrectionStillSpeed) * std::clamp((Shakiness - gravityCorrectionShakinessMinThreshold) / (gravityCorrectionShakinessMaxThreshold - gravityCorrectionShakinessMinThreshold), 0.f, 1.f);607}608else609{610gravCorrectionSpeed = Shakiness < gravityCorrectionShakinessMaxThreshold ? gravityCorrectionStillSpeed : gravityCorrectionShakySpeed;611}612// we also limit it to be no faster than a given proportion of the gyro rate, or the minimum gravity correction speed613const float gyroGravCorrectionLimit = std::max(angleSpeed * gravityCorrectionGyroFactor, gravityCorrectionMinimumSpeed);614if (gravCorrectionSpeed > gyroGravCorrectionLimit)615{616float closeEnoughFactor;617if (gravityCorrectionGyroMinThreshold < gravityCorrectionGyroMaxThreshold)618{619closeEnoughFactor = std::clamp((gravToAccel.Length() - gravityCorrectionGyroMinThreshold) / (gravityCorrectionGyroMaxThreshold - gravityCorrectionGyroMinThreshold), 0.f, 1.f);620}621else622{623closeEnoughFactor = gravToAccel.Length() < gravityCorrectionGyroMaxThreshold ? 0.f : 1.f;624}625gravCorrectionSpeed = gyroGravCorrectionLimit + (gravCorrectionSpeed - gyroGravCorrectionLimit) * closeEnoughFactor;626}627const Vec gravToAccelDelta = gravToAccelDir * gravCorrectionSpeed * deltaTime;628if (gravToAccelDelta.LengthSquared() < gravToAccel.LengthSquared())629{630Grav += gravToAccelDelta;631}632else633{634Grav = accelNorm * -gravityLength;635}636637const Vec gravityDirection = Grav.Normalized() * Quaternion.Inverse(); // absolute gravity direction638const float errorAngle = acosf(std::clamp(Vec(0.0f, -1.0f, 0.0f).Dot(gravityDirection), -1.f, 1.f));639const Vec flattened = Vec(0.0f, -1.0f, 0.0f).Cross(gravityDirection);640Quat correctionQuat = AngleAxis(errorAngle, flattened.x, flattened.y, flattened.z);641Quaternion = Quaternion * correctionQuat;642643Accel = accel + Grav;644}645else646{647Grav *= rotation.Inverse();648Accel = Grav;649}650Quaternion.Normalize();651}652653inline void Motion::SetSettings(GamepadMotionSettings* settings)654{655Settings = settings;656}657658inline SensorMinMaxWindow::SensorMinMaxWindow()659{660Reset(0.f);661}662663inline void SensorMinMaxWindow::Reset(float remainder)664{665NumSamples = 0;666TimeSampled = remainder;667}668669inline void SensorMinMaxWindow::AddSample(const Vec& inGyro, const Vec& inAccel, float deltaTime)670{671if (NumSamples == 0)672{673MaxGyro = inGyro;674MinGyro = inGyro;675MeanGyro = inGyro;676MaxAccel = inAccel;677MinAccel = inAccel;678MeanAccel = inAccel;679StartAccel = inAccel;680NumSamples = 1;681TimeSampled += deltaTime;682return;683}684685MaxGyro = MaxGyro.Max(inGyro);686MinGyro = MinGyro.Min(inGyro);687MaxAccel = MaxAccel.Max(inAccel);688MinAccel = MinAccel.Min(inAccel);689690NumSamples++;691TimeSampled += deltaTime;692693Vec delta = inGyro - MeanGyro;694MeanGyro += delta * (1.f / NumSamples);695delta = inAccel - MeanAccel;696MeanAccel += delta * (1.f / NumSamples);697}698699inline Vec SensorMinMaxWindow::GetMidGyro()700{701return MeanGyro;702}703704inline AutoCalibration::AutoCalibration()705{706CalibrationData = nullptr;707Reset();708}709710inline void AutoCalibration::Reset()711{712MinMaxWindow.Reset(0.f);713Confidence = 0.f;714bIsSteady = false;715MinDeltaGyro = Vec(1.f);716MinDeltaAccel = Vec(0.25f);717RecalibrateThreshold = 1.f;718SensorFusionSkippedTime = 0.f;719TimeSteadySensorFusion = 0.f;720TimeSteadyStillness = 0.f;721}722723inline bool AutoCalibration::AddSampleStillness(const Vec& inGyro, const Vec& inAccel, float deltaTime, bool doSensorFusion)724{725if (inGyro.x == 0.f && inGyro.y == 0.f && inGyro.z == 0.f &&726inAccel.x == 0.f && inAccel.y == 0.f && inAccel.z == 0.f)727{728// zeroes are almost certainly not valid inputs729return false;730}731732if (!Settings)733{734return false;735}736737if (!CalibrationData)738{739return false;740}741742// get settings743const int minStillnessSamples = Settings->MinStillnessSamples;744const float minStillnessCollectionTime = Settings->MinStillnessCollectionTime;745const float minStillnessCorrectionTime = Settings->MinStillnessCorrectionTime;746const float maxStillnessError = Settings->MaxStillnessError;747const float stillnessSampleDeteriorationRate = Settings->StillnessSampleDeteriorationRate;748const float stillnessErrorClimbRate = Settings->StillnessErrorClimbRate;749const float stillnessErrorDropOnRecalibrate = Settings->StillnessErrorDropOnRecalibrate;750const float stillnessCalibrationEaseInTime = Settings->StillnessCalibrationEaseInTime;751const float stillnessCalibrationHalfTime = Settings->StillnessCalibrationHalfTime * Confidence;752const float stillnessConfidenceRate = Settings->StillnessConfidenceRate;753const float stillnessGyroDelta = Settings->StillnessGyroDelta;754const float stillnessAccelDelta = Settings->StillnessAccelDelta;755756MinMaxWindow.AddSample(inGyro, inAccel, deltaTime);757// get deltas758const Vec gyroDelta = MinMaxWindow.MaxGyro - MinMaxWindow.MinGyro;759const Vec accelDelta = MinMaxWindow.MaxAccel - MinMaxWindow.MinAccel;760761bool calibrated = false;762bool isSteady = false;763const Vec climbThisTick = Vec(stillnessSampleDeteriorationRate * deltaTime);764if (stillnessGyroDelta < 0.f)765{766if (Confidence < 1.f)767{768MinDeltaGyro += climbThisTick;769}770}771else772{773MinDeltaGyro = Vec(stillnessGyroDelta);774}775if (stillnessAccelDelta < 0.f)776{777if (Confidence < 1.f)778{779MinDeltaAccel += climbThisTick;780}781}782else783{784MinDeltaAccel = Vec(stillnessAccelDelta);785}786787//printf("Deltas: %.4f %.4f %.4f; %.4f %.4f %.4f\n",788// gyroDelta.x, gyroDelta.y, gyroDelta.z,789// accelDelta.x, accelDelta.y, accelDelta.z);790791if (MinMaxWindow.NumSamples >= minStillnessSamples && MinMaxWindow.TimeSampled >= minStillnessCollectionTime)792{793MinDeltaGyro = MinDeltaGyro.Min(gyroDelta);794MinDeltaAccel = MinDeltaAccel.Min(accelDelta);795}796else797{798RecalibrateThreshold = std::min(RecalibrateThreshold + stillnessErrorClimbRate * deltaTime, maxStillnessError);799return false;800}801802// check that all inputs are below appropriate thresholds to be considered "still"803if (gyroDelta.x <= MinDeltaGyro.x * RecalibrateThreshold &&804gyroDelta.y <= MinDeltaGyro.y * RecalibrateThreshold &&805gyroDelta.z <= MinDeltaGyro.z * RecalibrateThreshold &&806accelDelta.x <= MinDeltaAccel.x * RecalibrateThreshold &&807accelDelta.y <= MinDeltaAccel.y * RecalibrateThreshold &&808accelDelta.z <= MinDeltaAccel.z * RecalibrateThreshold)809{810if (MinMaxWindow.NumSamples >= minStillnessSamples && MinMaxWindow.TimeSampled >= minStillnessCorrectionTime)811{812TimeSteadyStillness = std::min(TimeSteadyStillness + deltaTime, stillnessCalibrationEaseInTime);813const float calibrationEaseIn = stillnessCalibrationEaseInTime <= 0.f ? 1.f : TimeSteadyStillness / stillnessCalibrationEaseInTime;814815const Vec calibratedGyro = MinMaxWindow.GetMidGyro();816817const Vec oldGyroBias = Vec(CalibrationData->X, CalibrationData->Y, CalibrationData->Z) / std::max((float)CalibrationData->NumSamples, 1.f);818const float stillnessLerpFactor = stillnessCalibrationHalfTime <= 0.f ? 0.f : exp2f(-calibrationEaseIn * deltaTime / stillnessCalibrationHalfTime);819Vec newGyroBias = calibratedGyro.Lerp(oldGyroBias, stillnessLerpFactor);820Confidence = std::min(Confidence + deltaTime * stillnessConfidenceRate, 1.f);821isSteady = true;822823if (doSensorFusion)824{825const Vec previousNormal = MinMaxWindow.StartAccel.Normalized();826const Vec thisNormal = inAccel.Normalized();827Vec angularVelocity = thisNormal.Cross(previousNormal);828const float crossLength = angularVelocity.Length();829if (crossLength > 0.f)830{831const float thisDotPrev = std::clamp(thisNormal.Dot(previousNormal), -1.f, 1.f);832const float angleChange = acosf(thisDotPrev) * 180.0f / (float)M_PI;833const float anglePerSecond = angleChange / MinMaxWindow.TimeSampled;834angularVelocity *= anglePerSecond / crossLength;835}836837Vec axisCalibrationStrength = thisNormal.Abs();838Vec sensorFusionBias = (calibratedGyro - angularVelocity).Lerp(oldGyroBias, stillnessLerpFactor);839if (axisCalibrationStrength.x <= 0.7f)840{841newGyroBias.x = sensorFusionBias.x;842}843if (axisCalibrationStrength.y <= 0.7f)844{845newGyroBias.y = sensorFusionBias.y;846}847if (axisCalibrationStrength.z <= 0.7f)848{849newGyroBias.z = sensorFusionBias.z;850}851}852853CalibrationData->X = newGyroBias.x;854CalibrationData->Y = newGyroBias.y;855CalibrationData->Z = newGyroBias.z;856857CalibrationData->AccelMagnitude = MinMaxWindow.MeanAccel.Length();858CalibrationData->NumSamples = 1;859860calibrated = true;861}862else863{864RecalibrateThreshold = std::min(RecalibrateThreshold + stillnessErrorClimbRate * deltaTime, maxStillnessError);865}866}867else if (TimeSteadyStillness > 0.f)868{869//printf("Moved!\n");870RecalibrateThreshold -= stillnessErrorDropOnRecalibrate;871if (RecalibrateThreshold < 1.f) RecalibrateThreshold = 1.f;872873TimeSteadyStillness = 0.f;874MinMaxWindow.Reset(0.f);875}876else877{878RecalibrateThreshold = std::min(RecalibrateThreshold + stillnessErrorClimbRate * deltaTime, maxStillnessError);879MinMaxWindow.Reset(0.f);880}881882bIsSteady = isSteady;883return calibrated;884}885886inline void AutoCalibration::NoSampleStillness()887{888MinMaxWindow.Reset(0.f);889}890891inline bool AutoCalibration::AddSampleSensorFusion(const Vec& inGyro, const Vec& inAccel, float deltaTime)892{893if (deltaTime <= 0.f)894{895return false;896}897898if (inGyro.x == 0.f && inGyro.y == 0.f && inGyro.z == 0.f &&899inAccel.x == 0.f && inAccel.y == 0.f && inAccel.z == 0.f)900{901// all zeroes are almost certainly not valid inputs902TimeSteadySensorFusion = 0.f;903SensorFusionSkippedTime = 0.f;904PreviousAccel = inAccel;905SmoothedPreviousAccel = inAccel;906SmoothedAngularVelocityGyro = GamepadMotionHelpers::Vec();907SmoothedAngularVelocityAccel = GamepadMotionHelpers::Vec();908return false;909}910911if (PreviousAccel.x == 0.f && PreviousAccel.y == 0.f && PreviousAccel.z == 0.f)912{913TimeSteadySensorFusion = 0.f;914SensorFusionSkippedTime = 0.f;915PreviousAccel = inAccel;916SmoothedPreviousAccel = inAccel;917SmoothedAngularVelocityGyro = GamepadMotionHelpers::Vec();918SmoothedAngularVelocityAccel = GamepadMotionHelpers::Vec();919return false;920}921922// in case the controller state hasn't updated between samples923if (inAccel.x == PreviousAccel.x && inAccel.y == PreviousAccel.y && inAccel.z == PreviousAccel.z)924{925SensorFusionSkippedTime += deltaTime;926return false;927}928929if (!Settings)930{931return false;932}933934// get settings935const float sensorFusionCalibrationSmoothingStrength = Settings->SensorFusionCalibrationSmoothingStrength;936const float sensorFusionAngularAccelerationThreshold = Settings->SensorFusionAngularAccelerationThreshold;937const float sensorFusionCalibrationEaseInTime = Settings->SensorFusionCalibrationEaseInTime;938const float sensorFusionCalibrationHalfTime = Settings->SensorFusionCalibrationHalfTime * Confidence;939const float sensorFusionConfidenceRate = Settings->SensorFusionConfidenceRate;940941deltaTime += SensorFusionSkippedTime;942SensorFusionSkippedTime = 0.f;943bool calibrated = false;944bool isSteady = false;945946// framerate independent lerp smoothing: https://www.gamasutra.com/blogs/ScottLembcke/20180404/316046/Improved_Lerp_Smoothing.php947const float smoothingLerpFactor = exp2f(-sensorFusionCalibrationSmoothingStrength * deltaTime);948// velocity from smoothed accel matches better if we also smooth gyro949const Vec previousGyro = SmoothedAngularVelocityGyro;950SmoothedAngularVelocityGyro = inGyro.Lerp(SmoothedAngularVelocityGyro, smoothingLerpFactor); // smooth what remains951const float gyroAccelerationMag = (SmoothedAngularVelocityGyro - previousGyro).Length() / deltaTime;952// get angle between old and new accel953const Vec previousNormal = SmoothedPreviousAccel.Normalized();954const Vec thisAccel = inAccel.Lerp(SmoothedPreviousAccel, smoothingLerpFactor);955const Vec thisNormal = thisAccel.Normalized();956Vec angularVelocity = thisNormal.Cross(previousNormal);957const float crossLength = angularVelocity.Length();958if (crossLength > 0.f)959{960const float thisDotPrev = std::clamp(thisNormal.Dot(previousNormal), -1.f, 1.f);961const float angleChange = acosf(thisDotPrev) * 180.0f / (float)M_PI;962const float anglePerSecond = angleChange / deltaTime;963angularVelocity *= anglePerSecond / crossLength;964}965SmoothedAngularVelocityAccel = angularVelocity;966967// apply corrections968if (gyroAccelerationMag > sensorFusionAngularAccelerationThreshold || CalibrationData == nullptr)969{970TimeSteadySensorFusion = 0.f;971//printf("No calibration due to acceleration of %.4f\n", gyroAccelerationMag);972}973else974{975TimeSteadySensorFusion = std::min(TimeSteadySensorFusion + deltaTime, sensorFusionCalibrationEaseInTime);976const float calibrationEaseIn = sensorFusionCalibrationEaseInTime <= 0.f ? 1.f : TimeSteadySensorFusion / sensorFusionCalibrationEaseInTime;977const Vec oldGyroBias = Vec(CalibrationData->X, CalibrationData->Y, CalibrationData->Z) / std::max((float)CalibrationData->NumSamples, 1.f);978// recalibrate over time proportional to the difference between the calculated bias and the current assumed bias979const float sensorFusionLerpFactor = sensorFusionCalibrationHalfTime <= 0.f ? 0.f : exp2f(-calibrationEaseIn * deltaTime / sensorFusionCalibrationHalfTime);980Vec newGyroBias = (SmoothedAngularVelocityGyro - SmoothedAngularVelocityAccel).Lerp(oldGyroBias, sensorFusionLerpFactor);981Confidence = std::min(Confidence + deltaTime * sensorFusionConfidenceRate, 1.f);982isSteady = true;983// don't change bias in axes that can't be affected by the gravity direction984Vec axisCalibrationStrength = thisNormal.Abs();985if (axisCalibrationStrength.x > 0.7f)986{987axisCalibrationStrength.x = 1.f;988}989if (axisCalibrationStrength.y > 0.7f)990{991axisCalibrationStrength.y = 1.f;992}993if (axisCalibrationStrength.z > 0.7f)994{995axisCalibrationStrength.z = 1.f;996}997newGyroBias = newGyroBias.Lerp(oldGyroBias, axisCalibrationStrength.Min(Vec(1.f)));998999CalibrationData->X = newGyroBias.x;1000CalibrationData->Y = newGyroBias.y;1001CalibrationData->Z = newGyroBias.z;10021003CalibrationData->AccelMagnitude = thisAccel.Length();10041005CalibrationData->NumSamples = 1;10061007calibrated = true;10081009//printf("Recalibrating at a strength of %.4f\n", calibrationEaseIn);1010}10111012SmoothedPreviousAccel = thisAccel;1013PreviousAccel = inAccel;10141015//printf("Gyro: %.4f, %.4f, %.4f | Accel: %.4f, %.4f, %.4f\n",1016// SmoothedAngularVelocityGyro.x, SmoothedAngularVelocityGyro.y, SmoothedAngularVelocityGyro.z,1017// SmoothedAngularVelocityAccel.x, SmoothedAngularVelocityAccel.y, SmoothedAngularVelocityAccel.z);10181019bIsSteady = isSteady;10201021return calibrated;1022}10231024inline void AutoCalibration::NoSampleSensorFusion()1025{1026TimeSteadySensorFusion = 0.f;1027SensorFusionSkippedTime = 0.f;1028PreviousAccel = GamepadMotionHelpers::Vec();1029SmoothedPreviousAccel = GamepadMotionHelpers::Vec();1030SmoothedAngularVelocityGyro = GamepadMotionHelpers::Vec();1031SmoothedAngularVelocityAccel = GamepadMotionHelpers::Vec();1032}10331034inline void AutoCalibration::SetCalibrationData(GyroCalibration* calibrationData)1035{1036CalibrationData = calibrationData;1037}10381039inline void AutoCalibration::SetSettings(GamepadMotionSettings* settings)1040{1041Settings = settings;1042}10431044} // namespace GamepadMotionHelpers10451046inline GamepadMotion::GamepadMotion()1047{1048IsCalibrating = false;1049CurrentCalibrationMode = GamepadMotionHelpers::CalibrationMode::Manual;1050Reset();1051AutoCalibration.SetCalibrationData(&GyroCalibration);1052AutoCalibration.SetSettings(&Settings);1053Motion.SetSettings(&Settings);1054}10551056inline void GamepadMotion::Reset()1057{1058GyroCalibration = {};1059Gyro = {};1060RawAccel = {};1061Settings = GamepadMotionSettings();1062Motion.Reset();1063}10641065inline void GamepadMotion::ProcessMotion(float gyroX, float gyroY, float gyroZ,1066float accelX, float accelY, float accelZ, float deltaTime)1067{1068if (gyroX == 0.f && gyroY == 0.f && gyroZ == 0.f &&1069accelX == 0.f && accelY == 0.f && accelZ == 0.f)1070{1071// all zeroes are almost certainly not valid inputs1072return;1073}10741075float accelMagnitude = sqrtf(accelX * accelX + accelY * accelY + accelZ * accelZ);10761077if (IsCalibrating)1078{1079// manual calibration1080PushSensorSamples(gyroX, gyroY, gyroZ, accelMagnitude);1081AutoCalibration.NoSampleSensorFusion();1082AutoCalibration.NoSampleStillness();1083}1084else if (CurrentCalibrationMode & GamepadMotionHelpers::CalibrationMode::Stillness)1085{1086AutoCalibration.AddSampleStillness(GamepadMotionHelpers::Vec(gyroX, gyroY, gyroZ), GamepadMotionHelpers::Vec(accelX, accelY, accelZ), deltaTime, CurrentCalibrationMode & GamepadMotionHelpers::CalibrationMode::SensorFusion);1087AutoCalibration.NoSampleSensorFusion();1088}1089else1090{1091AutoCalibration.NoSampleStillness();1092if (CurrentCalibrationMode & GamepadMotionHelpers::CalibrationMode::SensorFusion)1093{1094AutoCalibration.AddSampleSensorFusion(GamepadMotionHelpers::Vec(gyroX, gyroY, gyroZ), GamepadMotionHelpers::Vec(accelX, accelY, accelZ), deltaTime);1095}1096else1097{1098AutoCalibration.NoSampleSensorFusion();1099}1100}11011102float gyroOffsetX, gyroOffsetY, gyroOffsetZ;1103GetCalibratedSensor(gyroOffsetX, gyroOffsetY, gyroOffsetZ, accelMagnitude);11041105gyroX -= gyroOffsetX;1106gyroY -= gyroOffsetY;1107gyroZ -= gyroOffsetZ;11081109Motion.Update(gyroX, gyroY, gyroZ, accelX, accelY, accelZ, accelMagnitude, deltaTime);11101111Gyro.x = gyroX;1112Gyro.y = gyroY;1113Gyro.z = gyroZ;1114RawAccel.x = accelX;1115RawAccel.y = accelY;1116RawAccel.z = accelZ;1117}11181119// reading the current state1120inline void GamepadMotion::GetCalibratedGyro(float& x, float& y, float& z)1121{1122x = Gyro.x;1123y = Gyro.y;1124z = Gyro.z;1125}11261127inline void GamepadMotion::GetGravity(float& x, float& y, float& z)1128{1129x = Motion.Grav.x;1130y = Motion.Grav.y;1131z = Motion.Grav.z;1132}11331134inline void GamepadMotion::GetProcessedAcceleration(float& x, float& y, float& z)1135{1136x = Motion.Accel.x;1137y = Motion.Accel.y;1138z = Motion.Accel.z;1139}11401141inline void GamepadMotion::GetOrientation(float& w, float& x, float& y, float& z)1142{1143w = Motion.Quaternion.w;1144x = Motion.Quaternion.x;1145y = Motion.Quaternion.y;1146z = Motion.Quaternion.z;1147}11481149inline void GamepadMotion::GetPlayerSpaceGyro(float& x, float& y, const float yawRelaxFactor)1150{1151CalculatePlayerSpaceGyro(x, y, Gyro.x, Gyro.y, Gyro.z, Motion.Grav.x, Motion.Grav.y, Motion.Grav.z, yawRelaxFactor);1152}11531154inline 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)1155{1156// 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#toc71157const float worldYaw = -(gravY * gyroY + gravZ * gyroZ);1158const float worldYawSign = worldYaw < 0.f ? -1.f : 1.f;1159y = worldYawSign * std::min(std::abs(worldYaw) * yawRelaxFactor, sqrtf(gyroY * gyroY + gyroZ * gyroZ));1160x = gyroX;1161}11621163inline void GamepadMotion::GetWorldSpaceGyro(float& x, float& y, const float sideReductionThreshold)1164{1165CalculateWorldSpaceGyro(x, y, Gyro.x, Gyro.y, Gyro.z, Motion.Grav.x, Motion.Grav.y, Motion.Grav.z, sideReductionThreshold);1166}11671168inline 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)1169{1170// 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#toc61171const float worldYaw = -gravX * gyroX - gravY * gyroY - gravZ * gyroZ;1172// project local pitch axis (X) onto gravity plane1173const float gravDotPitchAxis = gravX;1174GamepadMotionHelpers::Vec pitchAxis(1.f - gravX * gravDotPitchAxis,1175-gravY * gravDotPitchAxis,1176-gravZ * gravDotPitchAxis);1177// normalize1178const float pitchAxisLengthSquared = pitchAxis.LengthSquared();1179if (pitchAxisLengthSquared > 0.f)1180{1181const float pitchAxisLength = sqrtf(pitchAxisLengthSquared);1182const float lengthReciprocal = 1.f / pitchAxisLength;1183pitchAxis *= lengthReciprocal;11841185const float flatness = std::abs(gravY);1186const float upness = std::abs(gravZ);1187const float sideReduction = sideReductionThreshold <= 0.f ? 1.f : std::clamp((std::max(flatness, upness) - sideReductionThreshold) / sideReductionThreshold, 0.f, 1.f);11881189x = sideReduction * pitchAxis.Dot(GamepadMotionHelpers::Vec(gyroX, gyroY, gyroZ));1190}1191else1192{1193x = 0.f;1194}11951196y = worldYaw;1197}11981199// gyro calibration functions1200inline void GamepadMotion::StartContinuousCalibration()1201{1202IsCalibrating = true;1203}12041205inline void GamepadMotion::PauseContinuousCalibration()1206{1207IsCalibrating = false;1208}12091210inline void GamepadMotion::ResetContinuousCalibration()1211{1212GyroCalibration = {};1213AutoCalibration.Reset();1214}12151216inline void GamepadMotion::GetCalibrationOffset(float& xOffset, float& yOffset, float& zOffset)1217{1218float accelMagnitude;1219GetCalibratedSensor(xOffset, yOffset, zOffset, accelMagnitude);1220}12211222inline void GamepadMotion::SetCalibrationOffset(float xOffset, float yOffset, float zOffset, int weight)1223{1224if (GyroCalibration.NumSamples > 1)1225{1226GyroCalibration.AccelMagnitude *= ((float)weight) / GyroCalibration.NumSamples;1227}1228else1229{1230GyroCalibration.AccelMagnitude = (float)weight;1231}12321233GyroCalibration.NumSamples = weight;1234GyroCalibration.X = xOffset * weight;1235GyroCalibration.Y = yOffset * weight;1236GyroCalibration.Z = zOffset * weight;1237}12381239inline float GamepadMotion::GetAutoCalibrationConfidence()1240{1241return AutoCalibration.Confidence;1242}12431244inline void GamepadMotion::SetAutoCalibrationConfidence(float newConfidence)1245{1246AutoCalibration.Confidence = newConfidence;1247}12481249inline bool GamepadMotion::GetAutoCalibrationIsSteady()1250{1251return AutoCalibration.IsSteady();1252}12531254inline GamepadMotionHelpers::CalibrationMode GamepadMotion::GetCalibrationMode()1255{1256return CurrentCalibrationMode;1257}12581259inline void GamepadMotion::SetCalibrationMode(GamepadMotionHelpers::CalibrationMode calibrationMode)1260{1261CurrentCalibrationMode = calibrationMode;1262}12631264inline void GamepadMotion::ResetMotion()1265{1266Motion.Reset();1267}12681269// Private Methods12701271inline void GamepadMotion::PushSensorSamples(float gyroX, float gyroY, float gyroZ, float accelMagnitude)1272{1273// accumulate1274GyroCalibration.NumSamples++;1275GyroCalibration.X += gyroX;1276GyroCalibration.Y += gyroY;1277GyroCalibration.Z += gyroZ;1278GyroCalibration.AccelMagnitude += accelMagnitude;1279}12801281inline void GamepadMotion::GetCalibratedSensor(float& gyroOffsetX, float& gyroOffsetY, float& gyroOffsetZ, float& accelMagnitude)1282{1283if (GyroCalibration.NumSamples <= 0)1284{1285gyroOffsetX = 0.f;1286gyroOffsetY = 0.f;1287gyroOffsetZ = 0.f;1288accelMagnitude = 1.f;1289return;1290}12911292const float inverseSamples = 1.f / GyroCalibration.NumSamples;1293gyroOffsetX = GyroCalibration.X * inverseSamples;1294gyroOffsetY = GyroCalibration.Y * inverseSamples;1295gyroOffsetZ = GyroCalibration.Z * inverseSamples;1296accelMagnitude = GyroCalibration.AccelMagnitude * inverseSamples;1297}129812991300