// 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 #include // 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(static_cast(a) | static_cast(b)); } inline CalibrationMode operator&(CalibrationMode a, CalibrationMode b) { return static_cast(static_cast(a) & static_cast(b)); } inline CalibrationMode operator~(CalibrationMode a) { return static_cast(~static_cast(a)); } // https://stackoverflow.com/a/23152590/1130520 inline CalibrationMode& operator|=(CalibrationMode& a, CalibrationMode b) { return (CalibrationMode&)((int&)(a) |= static_cast(b)); } inline CalibrationMode& operator&=(CalibrationMode& a, CalibrationMode b) { return (CalibrationMode&)((int&)(a) &= static_cast(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; } /// /// 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) /// 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; }