From a7444aa99e0ee0d9f9f1956da997639b09cb5cf9 Mon Sep 17 00:00:00 2001 From: Boulay Mathias Date: Sun, 2 Jul 2023 17:25:27 +0200 Subject: [PATCH] Feat[gyro]: improve calibration and smoothing (#4229) * Feat[gyro]: Reduce jitter with moving average * Cleanup: Remove logging * Feat[gyro]: Add dampering window * Feat[gyro]: Use extremely low pass filter Technically, it doesn't entirely filter out the input, only stores it * Tweak[Gyro]: Lower hyro threshold even more * Feat: Simplify smoothing, better calibration * Feat[gyro]: Add dampering window # Conflicts: # app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/screens/LauncherPreferenceControlFragment.java # app_pojavlauncher/src/main/res/values/strings.xml * Cleanup[gyro]: Don't use import * Refactor[gyro]: reduce JNI calls * Fix[Gyro]: Adjust low pass treshold to sensitivity * Feat[Gyro]: Handle single/multi axis deplacement * Tweak[gyro]: Raise single axis threshold --- .../java/net/kdt/pojavlaunch/GyroControl.java | 177 +++++++++++++++--- .../net/kdt/pojavlaunch/MainActivity.java | 1 + .../prefs/LauncherPreferences.java | 3 + .../LauncherPreferenceControlFragment.java | 3 + .../src/main/res/values/strings.xml | 11 ++ .../src/main/res/xml/pref_control.xml | 6 + 6 files changed, 175 insertions(+), 26 deletions(-) diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/GyroControl.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/GyroControl.java index ce8b41e3e..e0fd35b71 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/GyroControl.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/GyroControl.java @@ -7,13 +7,20 @@ import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; import android.view.OrientationEventListener; +import android.view.Surface; import android.view.WindowManager; import net.kdt.pojavlaunch.prefs.LauncherPreferences; import org.lwjgl.glfw.CallbackBridge; -public class GyroControl implements SensorEventListener, GrabListener{ +import java.util.Arrays; + +public class GyroControl implements SensorEventListener, GrabListener { + /* How much distance has to be moved before taking into account the gyro */ + private static final float SINGLE_AXIS_LOW_PASS_THRESHOLD = 1.13F; + private static final float MULTI_AXIS_LOW_PASS_THRESHOLD = 1.3F; + private final WindowManager mWindowManager; private int mSurfaceRotation; private final SensorManager mSensorManager; @@ -29,12 +36,29 @@ public class GyroControl implements SensorEventListener, GrabListener{ private final float[] mCurrentRotation = new float[16]; private final float[] mAngleDifference = new float[3]; + + /* Used to average the last values, if smoothing is enabled */ + private final float[][] mAngleBuffer = new float[ + LauncherPreferences.PREF_GYRO_SMOOTHING ? 2 : 1 + ][3]; + private float xTotal = 0; + private float yTotal = 0; + + private float xAverage = 0; + private float yAverage = 0; + private int mHistoryIndex = -1; + + /* Store the gyro movement under the threshold */ + private float mStoredX = 0; + private float mStoredY = 0; + public GyroControl(Activity activity) { mWindowManager = activity.getWindowManager(); mSurfaceRotation = -10; mSensorManager = (SensorManager) activity.getSystemService(Context.SENSOR_SERVICE); mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_GAME_ROTATION_VECTOR); mCorrectionListener = new OrientationCorrectionListener(activity); + updateOrientation(); } public void enable() { @@ -50,8 +74,10 @@ public class GyroControl implements SensorEventListener, GrabListener{ if(mSensor == null) return; mSensorManager.unregisterListener(this); mCorrectionListener.disable(); + resetDamper(); CallbackBridge.removeGrabListener(this); } + @Override public void onSensorChanged(SensorEvent sensorEvent) { if (!mShouldHandleEvents) return; @@ -59,21 +85,78 @@ public class GyroControl implements SensorEventListener, GrabListener{ System.arraycopy(mCurrentRotation, 0, mPreviousRotation, 0, 16); SensorManager.getRotationMatrixFromVector(mCurrentRotation, sensorEvent.values); + if(mFirstPass){ // Setup initial position mFirstPass = false; return; } SensorManager.getAngleChange(mAngleDifference, mCurrentRotation, mPreviousRotation); + damperValue(mAngleDifference); + mStoredX += xAverage * 1000 * LauncherPreferences.PREF_GYRO_SENSITIVITY; + mStoredY += yAverage * 1000 * LauncherPreferences.PREF_GYRO_SENSITIVITY; - CallbackBridge.mouseX -= (mAngleDifference[mSwapXY ? 2 : 1] * 1000 * LauncherPreferences.PREF_GYRO_SENSITIVITY * xFactor); - CallbackBridge.mouseY += (mAngleDifference[mSwapXY ? 1 : 2] * 1000 * LauncherPreferences.PREF_GYRO_SENSITIVITY * yFactor); - CallbackBridge.sendCursorPos(CallbackBridge.mouseX, CallbackBridge.mouseY); + boolean updatePosition = false; + float absX = Math.abs(mStoredX); + float absY = Math.abs(mStoredY); + + if(absX + absY > MULTI_AXIS_LOW_PASS_THRESHOLD) { + CallbackBridge.mouseX -= ((mSwapXY ? mStoredY : mStoredX) * xFactor); + CallbackBridge.mouseY += ((mSwapXY ? mStoredX : mStoredY) * yFactor); + mStoredX = 0; + mStoredY = 0; + updatePosition = true; + } else { + if(Math.abs(mStoredX) > SINGLE_AXIS_LOW_PASS_THRESHOLD){ + CallbackBridge.mouseX -= ((mSwapXY ? mStoredY : mStoredX) * xFactor); + mStoredX = 0; + updatePosition = true; + } + + if(Math.abs(mStoredY) > SINGLE_AXIS_LOW_PASS_THRESHOLD) { + CallbackBridge.mouseY += ((mSwapXY ? mStoredX : mStoredY) * yFactor); + mStoredY = 0; + updatePosition = true; + } + } + + if(updatePosition){ + CallbackBridge.sendCursorPos(CallbackBridge.mouseX, CallbackBridge.mouseY); + } + } + + /** Update the axis mapping in accordance to activity rotation, used for initial rotation */ + public void updateOrientation(){ + int rotation = mWindowManager.getDefaultDisplay().getRotation(); + mSurfaceRotation = rotation; + switch (rotation){ + case Surface.ROTATION_0: + mSwapXY = true; + xFactor = 1; + yFactor = 1; + break; + case Surface.ROTATION_90: + mSwapXY = false; + xFactor = -1; + yFactor = 1; + break; + case Surface.ROTATION_180: + mSwapXY = true; + xFactor = -1; + yFactor = -1; + break; + case Surface.ROTATION_270: + mSwapXY = false; + xFactor = 1; + yFactor = -1; + break; + } + + if(LauncherPreferences.PREF_GYRO_INVERT_X) xFactor *= -1; + if(LauncherPreferences.PREF_GYRO_INVERT_Y) yFactor *= -1; } @Override - public void onAccuracyChanged(Sensor sensor, int i) { - - } + public void onAccuracyChanged(Sensor sensor, int i) {} @Override public void onGrabState(boolean isGrabbing) { @@ -81,6 +164,40 @@ public class GyroControl implements SensorEventListener, GrabListener{ mShouldHandleEvents = isGrabbing; } + + /** + * Compute the moving average of the gyroscope to reduce jitter + * @param newAngleDifference The new angle difference + */ + private void damperValue(float[] newAngleDifference){ + mHistoryIndex ++; + if(mHistoryIndex >= mAngleBuffer.length) mHistoryIndex = 0; + + xTotal -= mAngleBuffer[mHistoryIndex][1]; + yTotal -= mAngleBuffer[mHistoryIndex][2]; + + System.arraycopy(newAngleDifference, 0, mAngleBuffer[mHistoryIndex], 0, 3); + + xTotal += mAngleBuffer[mHistoryIndex][1]; + yTotal += mAngleBuffer[mHistoryIndex][2]; + + // compute the moving average + xAverage = xTotal / mAngleBuffer.length; + yAverage = yTotal / mAngleBuffer.length; + } + + /** Reset the moving average data */ + private void resetDamper(){ + mHistoryIndex = -1; + xTotal = 0; + yTotal = 0; + xAverage = 0; + yAverage = 0; + for(float[] oldAngle : mAngleBuffer){ + Arrays.fill(oldAngle, 0); + } + } + class OrientationCorrectionListener extends OrientationEventListener { public OrientationCorrectionListener(Context context) { @@ -92,31 +209,39 @@ public class GyroControl implements SensorEventListener, GrabListener{ // Force to wait to be in game before setting factors // Theoretically, one could use the whole interface in portrait... if(!mShouldHandleEvents) return; - int surfaceRotation = mWindowManager.getDefaultDisplay().getRotation(); - if(surfaceRotation == mSurfaceRotation) return; if(i == OrientationEventListener.ORIENTATION_UNKNOWN) { return; //change nothing } - mSurfaceRotation = surfaceRotation; - if((315 < i && i <= 360) || (i < 45) ) { - mSwapXY = true; - xFactor = 1; - yFactor = 1; - }else if(45 < i && i < 135) { - mSwapXY = false; - xFactor = 1; - yFactor = -1; - }else if(135 < i && i < 225) { - mSwapXY = true; - xFactor = -1; - yFactor = -1; - }else if(225 < i && i < 315) { - mSwapXY = false; - xFactor = -1; - yFactor = 1; + + + switch (mSurfaceRotation){ + case Surface.ROTATION_90: + case Surface.ROTATION_270: + mSwapXY = false; + if(225 < i && i < 315) { + xFactor = -1; + yFactor = 1; + }else if(45 < i && i < 135) { + xFactor = 1; + yFactor = -1; + } + break; + + case Surface.ROTATION_0: + case Surface.ROTATION_180: + mSwapXY = true; + if((315 < i && i <= 360) || (i < 45) ) { + xFactor = 1; + yFactor = 1; + }else if(135 < i && i < 225) { + xFactor = -1; + yFactor = -1; + } + break; } + if(LauncherPreferences.PREF_GYRO_INVERT_X) xFactor *= -1; if(LauncherPreferences.PREF_GYRO_INVERT_Y) yFactor *= -1; } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MainActivity.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MainActivity.java index 0741b5cea..68d8f3e83 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MainActivity.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MainActivity.java @@ -279,6 +279,7 @@ public class MainActivity extends BaseActivity implements ControlButtonMenuListe public void onConfigurationChanged(@NonNull Configuration newConfig) { super.onConfigurationChanged(newConfig); + if(mGyroControl != null) mGyroControl.updateOrientation(); Tools.updateWindowSize(this); minecraftGLView.refreshSize(); runOnUiThread(() -> mControlLayout.refreshControlButtonPositions()); diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/LauncherPreferences.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/LauncherPreferences.java index 232eb1445..60c216b58 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/LauncherPreferences.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/LauncherPreferences.java @@ -49,6 +49,7 @@ public class LauncherPreferences { public static boolean PREF_ENABLE_GYRO = false; public static float PREF_GYRO_SENSITIVITY = 1f; public static int PREF_GYRO_SAMPLE_RATE = 16; + public static boolean PREF_GYRO_SMOOTHING = true; public static boolean PREF_GYRO_INVERT_X = false; @@ -61,6 +62,7 @@ public class LauncherPreferences { public static boolean PREF_BIG_CORE_AFFINITY = false; + public static void loadPreferences(Context ctx) { //Required for the data folder. Tools.initContextConstants(ctx); @@ -93,6 +95,7 @@ public class LauncherPreferences { PREF_ENABLE_GYRO = DEFAULT_PREF.getBoolean("enableGyro", false); PREF_GYRO_SENSITIVITY = ((float)DEFAULT_PREF.getInt("gyroSensitivity", 100))/100f; PREF_GYRO_SAMPLE_RATE = DEFAULT_PREF.getInt("gyroSampleRate", 16); + PREF_GYRO_SMOOTHING = DEFAULT_PREF.getBoolean("gyroSmoothing", true); PREF_GYRO_INVERT_X = DEFAULT_PREF.getBoolean("gyroInvertX", false); PREF_GYRO_INVERT_Y = DEFAULT_PREF.getBoolean("gyroInvertY", false); PREF_FORCE_VSYNC = DEFAULT_PREF.getBoolean("force_vsync", false); diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/screens/LauncherPreferenceControlFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/screens/LauncherPreferenceControlFragment.java index ed11ec071..d380bb7c9 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/screens/LauncherPreferenceControlFragment.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/screens/LauncherPreferenceControlFragment.java @@ -25,6 +25,7 @@ public class LauncherPreferenceControlFragment extends LauncherPreferenceFragmen float gyroSpeed = LauncherPreferences.PREF_GYRO_SENSITIVITY; float joystickDeadzone = LauncherPreferences.PREF_DEADZONE_SCALE; + //Triggers a write for some reason which resets the value addPreferencesFromResource(R.xml.pref_control); @@ -53,6 +54,7 @@ public class LauncherPreferenceControlFragment extends LauncherPreferenceFragmen deadzoneSeek.setValue((int) joystickDeadzone * 100); deadzoneSeek.setSuffix(" %"); + Context context = getContext(); if(context != null) { mGyroAvailable = ((SensorManager)context.getSystemService(Context.SENSOR_SERVICE)).getDefaultSensor(Sensor.TYPE_GYROSCOPE) != null; @@ -83,6 +85,7 @@ public class LauncherPreferenceControlFragment extends LauncherPreferenceFragmen findPreference("gyroSampleRate").setVisible(LauncherPreferences.PREF_ENABLE_GYRO); findPreference("gyroInvertX").setVisible(LauncherPreferences.PREF_ENABLE_GYRO); findPreference("gyroInvertY").setVisible(LauncherPreferences.PREF_ENABLE_GYRO); + findPreference("gyroSmoothing").setVisible(LauncherPreferences.PREF_ENABLE_GYRO); } } diff --git a/app_pojavlauncher/src/main/res/values/strings.xml b/app_pojavlauncher/src/main/res/values/strings.xml index 5f75caf87..9f571caf3 100644 --- a/app_pojavlauncher/src/main/res/values/strings.xml +++ b/app_pojavlauncher/src/main/res/values/strings.xml @@ -302,6 +302,8 @@ Invert Y axis Invert the horizontal axis Invert the vertical axis + Enable gyro smoothing + Reduce jitter in exchange of latency. Back to the last screen Don\'t shrink textures @@ -367,12 +369,15 @@ Save and exit Yes No + The controller config has been wiped Controller settings Reset controller mapping Allow you to remap the controller buttons Joystick deadzone scale Increase it if the joystick drifts + + Force renderer to run on the big core Forces the Minecraft render thread to run on the core with the highest max frequency Select a version @@ -395,4 +400,10 @@ Failed to collect data for OptiFine installation Downloading %s Create OptiFine profile + + + Smoothing time + Reduce jitter in exchange of latency. 0 to disable it + + diff --git a/app_pojavlauncher/src/main/res/xml/pref_control.xml b/app_pojavlauncher/src/main/res/xml/pref_control.xml index 73e26380d..0f333095e 100644 --- a/app_pojavlauncher/src/main/res/xml/pref_control.xml +++ b/app_pojavlauncher/src/main/res/xml/pref_control.xml @@ -112,6 +112,12 @@ android:summary="@string/preference_gyro_sample_rate_description" app2:selectable="false" app2:showSeekBarValue="true"/> +