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
This commit is contained in:
Boulay Mathias 2023-07-02 17:25:27 +02:00 committed by GitHub
parent 2a27439887
commit a7444aa99e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 175 additions and 26 deletions

View File

@ -7,13 +7,20 @@ import android.hardware.SensorEvent;
import android.hardware.SensorEventListener; import android.hardware.SensorEventListener;
import android.hardware.SensorManager; import android.hardware.SensorManager;
import android.view.OrientationEventListener; import android.view.OrientationEventListener;
import android.view.Surface;
import android.view.WindowManager; import android.view.WindowManager;
import net.kdt.pojavlaunch.prefs.LauncherPreferences; import net.kdt.pojavlaunch.prefs.LauncherPreferences;
import org.lwjgl.glfw.CallbackBridge; 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 final WindowManager mWindowManager;
private int mSurfaceRotation; private int mSurfaceRotation;
private final SensorManager mSensorManager; private final SensorManager mSensorManager;
@ -29,12 +36,29 @@ public class GyroControl implements SensorEventListener, GrabListener{
private final float[] mCurrentRotation = new float[16]; private final float[] mCurrentRotation = new float[16];
private final float[] mAngleDifference = new float[3]; 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) { public GyroControl(Activity activity) {
mWindowManager = activity.getWindowManager(); mWindowManager = activity.getWindowManager();
mSurfaceRotation = -10; mSurfaceRotation = -10;
mSensorManager = (SensorManager) activity.getSystemService(Context.SENSOR_SERVICE); mSensorManager = (SensorManager) activity.getSystemService(Context.SENSOR_SERVICE);
mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_GAME_ROTATION_VECTOR); mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_GAME_ROTATION_VECTOR);
mCorrectionListener = new OrientationCorrectionListener(activity); mCorrectionListener = new OrientationCorrectionListener(activity);
updateOrientation();
} }
public void enable() { public void enable() {
@ -50,8 +74,10 @@ public class GyroControl implements SensorEventListener, GrabListener{
if(mSensor == null) return; if(mSensor == null) return;
mSensorManager.unregisterListener(this); mSensorManager.unregisterListener(this);
mCorrectionListener.disable(); mCorrectionListener.disable();
resetDamper();
CallbackBridge.removeGrabListener(this); CallbackBridge.removeGrabListener(this);
} }
@Override @Override
public void onSensorChanged(SensorEvent sensorEvent) { public void onSensorChanged(SensorEvent sensorEvent) {
if (!mShouldHandleEvents) return; if (!mShouldHandleEvents) return;
@ -59,21 +85,78 @@ public class GyroControl implements SensorEventListener, GrabListener{
System.arraycopy(mCurrentRotation, 0, mPreviousRotation, 0, 16); System.arraycopy(mCurrentRotation, 0, mPreviousRotation, 0, 16);
SensorManager.getRotationMatrixFromVector(mCurrentRotation, sensorEvent.values); SensorManager.getRotationMatrixFromVector(mCurrentRotation, sensorEvent.values);
if(mFirstPass){ // Setup initial position if(mFirstPass){ // Setup initial position
mFirstPass = false; mFirstPass = false;
return; return;
} }
SensorManager.getAngleChange(mAngleDifference, mCurrentRotation, mPreviousRotation); 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); boolean updatePosition = false;
CallbackBridge.mouseY += (mAngleDifference[mSwapXY ? 1 : 2] * 1000 * LauncherPreferences.PREF_GYRO_SENSITIVITY * yFactor); float absX = Math.abs(mStoredX);
CallbackBridge.sendCursorPos(CallbackBridge.mouseX, CallbackBridge.mouseY); 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 @Override
public void onAccuracyChanged(Sensor sensor, int i) { public void onAccuracyChanged(Sensor sensor, int i) {}
}
@Override @Override
public void onGrabState(boolean isGrabbing) { public void onGrabState(boolean isGrabbing) {
@ -81,6 +164,40 @@ public class GyroControl implements SensorEventListener, GrabListener{
mShouldHandleEvents = isGrabbing; 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 { class OrientationCorrectionListener extends OrientationEventListener {
public OrientationCorrectionListener(Context context) { public OrientationCorrectionListener(Context context) {
@ -92,31 +209,39 @@ public class GyroControl implements SensorEventListener, GrabListener{
// Force to wait to be in game before setting factors // Force to wait to be in game before setting factors
// Theoretically, one could use the whole interface in portrait... // Theoretically, one could use the whole interface in portrait...
if(!mShouldHandleEvents) return; if(!mShouldHandleEvents) return;
int surfaceRotation = mWindowManager.getDefaultDisplay().getRotation();
if(surfaceRotation == mSurfaceRotation) return;
if(i == OrientationEventListener.ORIENTATION_UNKNOWN) { if(i == OrientationEventListener.ORIENTATION_UNKNOWN) {
return; //change nothing return; //change nothing
} }
mSurfaceRotation = surfaceRotation;
if((315 < i && i <= 360) || (i < 45) ) {
mSwapXY = true;
xFactor = 1; switch (mSurfaceRotation){
yFactor = 1; case Surface.ROTATION_90:
}else if(45 < i && i < 135) { case Surface.ROTATION_270:
mSwapXY = false; mSwapXY = false;
xFactor = 1; if(225 < i && i < 315) {
yFactor = -1; xFactor = -1;
}else if(135 < i && i < 225) { yFactor = 1;
mSwapXY = true; }else if(45 < i && i < 135) {
xFactor = -1; xFactor = 1;
yFactor = -1; yFactor = -1;
}else if(225 < i && i < 315) { }
mSwapXY = false; break;
xFactor = -1;
yFactor = 1; 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_X) xFactor *= -1;
if(LauncherPreferences.PREF_GYRO_INVERT_Y) yFactor *= -1; if(LauncherPreferences.PREF_GYRO_INVERT_Y) yFactor *= -1;
} }

View File

@ -279,6 +279,7 @@ public class MainActivity extends BaseActivity implements ControlButtonMenuListe
public void onConfigurationChanged(@NonNull Configuration newConfig) { public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig); super.onConfigurationChanged(newConfig);
if(mGyroControl != null) mGyroControl.updateOrientation();
Tools.updateWindowSize(this); Tools.updateWindowSize(this);
minecraftGLView.refreshSize(); minecraftGLView.refreshSize();
runOnUiThread(() -> mControlLayout.refreshControlButtonPositions()); runOnUiThread(() -> mControlLayout.refreshControlButtonPositions());

View File

@ -49,6 +49,7 @@ public class LauncherPreferences {
public static boolean PREF_ENABLE_GYRO = false; public static boolean PREF_ENABLE_GYRO = false;
public static float PREF_GYRO_SENSITIVITY = 1f; public static float PREF_GYRO_SENSITIVITY = 1f;
public static int PREF_GYRO_SAMPLE_RATE = 16; public static int PREF_GYRO_SAMPLE_RATE = 16;
public static boolean PREF_GYRO_SMOOTHING = true;
public static boolean PREF_GYRO_INVERT_X = false; 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 boolean PREF_BIG_CORE_AFFINITY = false;
public static void loadPreferences(Context ctx) { public static void loadPreferences(Context ctx) {
//Required for the data folder. //Required for the data folder.
Tools.initContextConstants(ctx); Tools.initContextConstants(ctx);
@ -93,6 +95,7 @@ public class LauncherPreferences {
PREF_ENABLE_GYRO = DEFAULT_PREF.getBoolean("enableGyro", false); PREF_ENABLE_GYRO = DEFAULT_PREF.getBoolean("enableGyro", false);
PREF_GYRO_SENSITIVITY = ((float)DEFAULT_PREF.getInt("gyroSensitivity", 100))/100f; PREF_GYRO_SENSITIVITY = ((float)DEFAULT_PREF.getInt("gyroSensitivity", 100))/100f;
PREF_GYRO_SAMPLE_RATE = DEFAULT_PREF.getInt("gyroSampleRate", 16); 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_X = DEFAULT_PREF.getBoolean("gyroInvertX", false);
PREF_GYRO_INVERT_Y = DEFAULT_PREF.getBoolean("gyroInvertY", false); PREF_GYRO_INVERT_Y = DEFAULT_PREF.getBoolean("gyroInvertY", false);
PREF_FORCE_VSYNC = DEFAULT_PREF.getBoolean("force_vsync", false); PREF_FORCE_VSYNC = DEFAULT_PREF.getBoolean("force_vsync", false);

View File

@ -25,6 +25,7 @@ public class LauncherPreferenceControlFragment extends LauncherPreferenceFragmen
float gyroSpeed = LauncherPreferences.PREF_GYRO_SENSITIVITY; float gyroSpeed = LauncherPreferences.PREF_GYRO_SENSITIVITY;
float joystickDeadzone = LauncherPreferences.PREF_DEADZONE_SCALE; float joystickDeadzone = LauncherPreferences.PREF_DEADZONE_SCALE;
//Triggers a write for some reason which resets the value //Triggers a write for some reason which resets the value
addPreferencesFromResource(R.xml.pref_control); addPreferencesFromResource(R.xml.pref_control);
@ -53,6 +54,7 @@ public class LauncherPreferenceControlFragment extends LauncherPreferenceFragmen
deadzoneSeek.setValue((int) joystickDeadzone * 100); deadzoneSeek.setValue((int) joystickDeadzone * 100);
deadzoneSeek.setSuffix(" %"); deadzoneSeek.setSuffix(" %");
Context context = getContext(); Context context = getContext();
if(context != null) { if(context != null) {
mGyroAvailable = ((SensorManager)context.getSystemService(Context.SENSOR_SERVICE)).getDefaultSensor(Sensor.TYPE_GYROSCOPE) != 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("gyroSampleRate").setVisible(LauncherPreferences.PREF_ENABLE_GYRO);
findPreference("gyroInvertX").setVisible(LauncherPreferences.PREF_ENABLE_GYRO); findPreference("gyroInvertX").setVisible(LauncherPreferences.PREF_ENABLE_GYRO);
findPreference("gyroInvertY").setVisible(LauncherPreferences.PREF_ENABLE_GYRO); findPreference("gyroInvertY").setVisible(LauncherPreferences.PREF_ENABLE_GYRO);
findPreference("gyroSmoothing").setVisible(LauncherPreferences.PREF_ENABLE_GYRO);
} }
} }

View File

@ -302,6 +302,8 @@
<string name="preference_gyro_invert_y_axis">Invert Y axis</string> <string name="preference_gyro_invert_y_axis">Invert Y axis</string>
<string name="preference_gyro_invert_x_axis_description">Invert the horizontal axis</string> <string name="preference_gyro_invert_x_axis_description">Invert the horizontal axis</string>
<string name="preference_gyro_invert_y_axis_description">Invert the vertical axis</string> <string name="preference_gyro_invert_y_axis_description">Invert the vertical axis</string>
<string name="preference_gyro_smoothing_title">Enable gyro smoothing</string>
<string name="preference_gyro_smoothing_description">Reduce jitter in exchange of latency.</string>
<string name="preference_back_title">Back to the last screen</string> <string name="preference_back_title">Back to the last screen</string>
<string name="gles_hack_none">Don\'t shrink textures</string> <string name="gles_hack_none">Don\'t shrink textures</string>
@ -367,12 +369,15 @@
<string name="global_save_and_exit">Save and exit</string> <string name="global_save_and_exit">Save and exit</string>
<string name="global_yes">Yes</string> <string name="global_yes">Yes</string>
<string name="global_no">No</string> <string name="global_no">No</string>
<string name="preference_controller_map_wiped">The controller config has been wiped</string> <string name="preference_controller_map_wiped">The controller config has been wiped</string>
<string name="preference_category_controller_settings">Controller settings</string> <string name="preference_category_controller_settings">Controller settings</string>
<string name="preference_wipe_controller_title">Reset controller mapping</string> <string name="preference_wipe_controller_title">Reset controller mapping</string>
<string name="preference_wipe_controller_description">Allow you to remap the controller buttons</string> <string name="preference_wipe_controller_description">Allow you to remap the controller buttons</string>
<string name="preference_deadzone_scale_title">Joystick deadzone scale</string> <string name="preference_deadzone_scale_title">Joystick deadzone scale</string>
<string name="preference_deadzone_scale_description">Increase it if the joystick drifts</string> <string name="preference_deadzone_scale_description">Increase it if the joystick drifts</string>
<string name="preference_force_big_core_title">Force renderer to run on the big core</string> <string name="preference_force_big_core_title">Force renderer to run on the big core</string>
<string name="preference_force_big_core_desc">Forces the Minecraft render thread to run on the core with the highest max frequency</string> <string name="preference_force_big_core_desc">Forces the Minecraft render thread to run on the core with the highest max frequency</string>
<string name="version_select_hint">Select a version</string> <string name="version_select_hint">Select a version</string>
@ -395,4 +400,10 @@
<string name="of_dl_failed_to_scrape">Failed to collect data for OptiFine installation</string> <string name="of_dl_failed_to_scrape">Failed to collect data for OptiFine installation</string>
<string name="of_dl_progress">Downloading %s</string> <string name="of_dl_progress">Downloading %s</string>
<string name="create_profile_optifine">Create OptiFine profile</string> <string name="create_profile_optifine">Create OptiFine profile</string>
<string name="preference_gyro_damper_window_title">Smoothing time</string>
<string name="preference_gyro_damper_window_description">Reduce jitter in exchange of latency. 0 to disable it</string>
</resources> </resources>

View File

@ -112,6 +112,12 @@
android:summary="@string/preference_gyro_sample_rate_description" android:summary="@string/preference_gyro_sample_rate_description"
app2:selectable="false" app2:selectable="false"
app2:showSeekBarValue="true"/> app2:showSeekBarValue="true"/>
<SwitchPreferenceCompat
android:key="gyroSmoothing"
android:title="@string/preference_gyro_smoothing_title"
android:summary="@string/preference_gyro_smoothing_description"
android:defaultValue="true"
/>
<SwitchPreference <SwitchPreference
android:key="gyroInvertX" android:key="gyroInvertX"
android:title="@string/preference_gyro_invert_x_axis" android:title="@string/preference_gyro_invert_x_axis"