Recenter SphericalSurfaceView view on startup

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=214261973
This commit is contained in:
eguven 2018-09-24 07:09:26 -07:00 committed by Oliver Woodman
parent c934ce744d
commit e86d5efb21
7 changed files with 152 additions and 90 deletions

View File

@ -61,7 +61,7 @@ public final class FrameRotationQueue {
* timestamp to {@code matrix}. Removes all older rotations and the returned one from the queue.
* Does nothing if there is no such rotation.
*
* @param matrix A float array to hold the rotation matrix.
* @param matrix The rotation matrix.
* @param timestampUs The time in microseconds to query the rotation.
* @return Whether a rotation matrix is copied to {@code matrix}.
*/
@ -83,8 +83,11 @@ public final class FrameRotationQueue {
/**
* Computes a recentering matrix from the given angle-axis rotation only accounting for yaw. Roll
* and tilt will not be compensated.
*
* @param recenterMatrix The recenter matrix.
* @param rotationMatrix The rotation matrix.
*/
private static void computeRecenterMatrix(float[] recenterMatrix, float[] rotationMatrix) {
public static void computeRecenterMatrix(float[] recenterMatrix, float[] rotationMatrix) {
// The re-centering matrix is computed as follows:
// recenter.row(2) = temp.col(2).transpose();
// recenter.row(0) = recenter.row(1).cross(recenter.row(2)).normalized();

View File

@ -30,7 +30,7 @@ import java.nio.FloatBuffer;
import java.nio.IntBuffer;
/** GL utility methods. */
/*package*/ final class GlUtil {
/* package */ final class GlUtil {
private static final String TAG = "Spherical.Utils";
/** Class only contains static methods. */
@ -79,7 +79,9 @@ import java.nio.IntBuffer;
int program = GLES20.glCreateProgram();
GLES20.glAttachShader(program, vertexShader);
GLES20.glDeleteShader(vertexShader);
GLES20.glAttachShader(program, fragmentShader);
GLES20.glDeleteShader(fragmentShader);
// Link and check for errors.
GLES20.glLinkProgram(program);

View File

@ -0,0 +1,126 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ui.spherical;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.opengl.Matrix;
import android.support.annotation.BinderThread;
import android.view.Display;
import android.view.Surface;
import com.google.android.exoplayer2.video.spherical.FrameRotationQueue;
/**
* Listens for orientation sensor events, converts event data to rotation matrix and roll value, and
* notifies its own listeners.
*/
/* package */ final class OrientationListener implements SensorEventListener {
/** A listener for orientation changes. */
public interface Listener {
/**
* Called on device orientation change.
*
* @param deviceOrientationMatrix A 4x4 matrix defining device orientation.
* @param deviceRoll Device roll value, in radians. The range of values is -π/2 to π/2.
*/
void onOrientationChange(float[] deviceOrientationMatrix, float deviceRoll);
}
private final float[] deviceOrientationMatrix4x4 = new float[16];
private final float[] tempMatrix4x4 = new float[16];
private final float[] recenterMatrix4x4 = new float[16];
private final float[] angles = new float[3];
private final Display display;
private final Listener[] listeners;
private boolean recenterMatrixComputed;
public OrientationListener(Display display, Listener... listeners) {
this.display = display;
this.listeners = listeners;
}
@Override
@BinderThread
public void onSensorChanged(SensorEvent event) {
SensorManager.getRotationMatrixFromVector(deviceOrientationMatrix4x4, event.values);
rotateAroundZ(deviceOrientationMatrix4x4, display.getRotation());
float roll = extractRoll(deviceOrientationMatrix4x4);
// Rotation vector sensor assumes Y is parallel to the ground.
rotateYtoSky(deviceOrientationMatrix4x4);
recenter(deviceOrientationMatrix4x4);
notifyListeners(deviceOrientationMatrix4x4, roll);
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// Do nothing.
}
private void notifyListeners(float[] deviceOrientationMatrix, float roll) {
for (Listener listener : listeners) {
listener.onOrientationChange(deviceOrientationMatrix, roll);
}
}
private void recenter(float[] matrix) {
if (!recenterMatrixComputed) {
FrameRotationQueue.computeRecenterMatrix(recenterMatrix4x4, matrix);
recenterMatrixComputed = true;
}
System.arraycopy(matrix, 0, tempMatrix4x4, 0, tempMatrix4x4.length);
Matrix.multiplyMM(matrix, 0, tempMatrix4x4, 0, recenterMatrix4x4, 0);
}
private float extractRoll(float[] matrix) {
// Remapping is required since we need the calculated roll of the phone to be independent of the
// phone's pitch & yaw.
SensorManager.remapCoordinateSystem(
matrix, SensorManager.AXIS_X, SensorManager.AXIS_MINUS_Z, tempMatrix4x4);
SensorManager.getOrientation(tempMatrix4x4, angles);
return angles[2];
}
private void rotateAroundZ(float[] matrix, int rotation) {
int xAxis;
int yAxis;
switch (rotation) {
case Surface.ROTATION_270:
xAxis = SensorManager.AXIS_MINUS_Y;
yAxis = SensorManager.AXIS_X;
break;
case Surface.ROTATION_180:
xAxis = SensorManager.AXIS_MINUS_X;
yAxis = SensorManager.AXIS_MINUS_Y;
break;
case Surface.ROTATION_90:
xAxis = SensorManager.AXIS_Y;
yAxis = SensorManager.AXIS_MINUS_X;
break;
case Surface.ROTATION_0:
return;
default:
throw new IllegalStateException();
}
System.arraycopy(matrix, 0, tempMatrix4x4, 0, tempMatrix4x4.length);
SensorManager.remapCoordinateSystem(tempMatrix4x4, xAxis, yAxis, matrix);
}
private static void rotateYtoSky(float[] matrix) {
Matrix.rotateM(matrix, 0, 90, 1, 0, 0);
}
}

View File

@ -30,10 +30,10 @@ import org.checkerframework.checker.nullness.qual.Nullable;
* thread when ready.
*/
@TargetApi(15)
/*package*/ final class ProjectionRenderer {
/* package */ final class ProjectionRenderer {
/** Defines the constants identifying the current eye type. */
/*package*/ interface EyeType {
/* package */ interface EyeType {
/** Single eye in monocular rendering. */
int MONOCULAR = 0;

View File

@ -36,7 +36,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** Renders a GL Scene. */
/*package*/ class SceneRenderer implements VideoFrameMetadataListener, CameraMotionListener {
/* package */ class SceneRenderer implements VideoFrameMetadataListener, CameraMotionListener {
private final AtomicBoolean frameAvailable;
private final AtomicBoolean resetRotationAtNextFrame;

View File

@ -20,8 +20,6 @@ import android.content.Context;
import android.graphics.PointF;
import android.graphics.SurfaceTexture;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
@ -79,11 +77,11 @@ public final class SphericalSurfaceView extends GLSurfaceView {
// TODO Calculate this depending on surface size and field of view.
private static final float PX_PER_DEGREES = 25;
/*package*/ static final float UPRIGHT_ROLL = (float) Math.PI;
/* package */ static final float UPRIGHT_ROLL = (float) Math.PI;
private final SensorManager sensorManager;
private final @Nullable Sensor orientationSensor;
private final PhoneOrientationListener phoneOrientationListener;
private final OrientationListener orientationListener;
private final Renderer renderer;
private final Handler mainHandler;
private final TouchTracker touchTracker;
@ -117,7 +115,7 @@ public final class SphericalSurfaceView extends GLSurfaceView {
touchTracker = new TouchTracker(context, renderer, PX_PER_DEGREES);
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Display display = Assertions.checkNotNull(windowManager).getDefaultDisplay();
phoneOrientationListener = new PhoneOrientationListener(display, touchTracker, renderer);
orientationListener = new OrientationListener(display, touchTracker, renderer);
setEGLContextClientVersion(2);
setRenderer(renderer);
@ -173,14 +171,14 @@ public final class SphericalSurfaceView extends GLSurfaceView {
super.onResume();
if (orientationSensor != null) {
sensorManager.registerListener(
phoneOrientationListener, orientationSensor, SensorManager.SENSOR_DELAY_FASTEST);
orientationListener, orientationSensor, SensorManager.SENSOR_DELAY_FASTEST);
}
}
@Override
public void onPause() {
if (orientationSensor != null) {
sensorManager.unregisterListener(phoneOrientationListener);
sensorManager.unregisterListener(orientationListener);
}
super.onPause();
}
@ -229,82 +227,13 @@ public final class SphericalSurfaceView extends GLSurfaceView {
}
}
/** Detects sensor events and saves them as a matrix. */
private static class PhoneOrientationListener implements SensorEventListener {
private final float[] phoneInWorldSpaceMatrix = new float[16];
private final float[] remappedPhoneMatrix = new float[16];
private final float[] angles = new float[3];
private final Display display;
private final TouchTracker touchTracker;
private final Renderer renderer;
public PhoneOrientationListener(Display display, TouchTracker touchTracker, Renderer renderer) {
this.display = display;
this.touchTracker = touchTracker;
this.renderer = renderer;
}
@Override
@BinderThread
public void onSensorChanged(SensorEvent event) {
SensorManager.getRotationMatrixFromVector(remappedPhoneMatrix, event.values);
// If we're not in upright portrait mode, remap the axes of the coordinate system according to
// the display rotation.
int xAxis;
int yAxis;
switch (display.getRotation()) {
case Surface.ROTATION_270:
xAxis = SensorManager.AXIS_MINUS_Y;
yAxis = SensorManager.AXIS_X;
break;
case Surface.ROTATION_180:
xAxis = SensorManager.AXIS_MINUS_X;
yAxis = SensorManager.AXIS_MINUS_Y;
break;
case Surface.ROTATION_90:
xAxis = SensorManager.AXIS_Y;
yAxis = SensorManager.AXIS_MINUS_X;
break;
case Surface.ROTATION_0:
default:
xAxis = SensorManager.AXIS_X;
yAxis = SensorManager.AXIS_Y;
break;
}
SensorManager.remapCoordinateSystem(
remappedPhoneMatrix, xAxis, yAxis, phoneInWorldSpaceMatrix);
// Extract the phone's roll and pass it on to touchTracker & renderer. Remapping is required
// since we need the calculated roll of the phone to be independent of the phone's pitch &
// yaw. Any operation that decomposes rotation to Euler angles needs to be performed
// carefully.
SensorManager.remapCoordinateSystem(
phoneInWorldSpaceMatrix,
SensorManager.AXIS_X,
SensorManager.AXIS_MINUS_Z,
remappedPhoneMatrix);
SensorManager.getOrientation(remappedPhoneMatrix, angles);
float roll = angles[2];
touchTracker.setRoll(roll);
// Rotate from Android coordinates to OpenGL coordinates. Android's coordinate system
// assumes Y points North and Z points to the sky. OpenGL has Y pointing up and Z pointing
// toward the user.
Matrix.rotateM(phoneInWorldSpaceMatrix, 0, 90, 1, 0, 0);
renderer.setDeviceOrientation(phoneInWorldSpaceMatrix, roll);
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {}
}
/**
* Standard GL Renderer implementation. The notable code is the matrix multiplication in
* onDrawFrame and updatePitchMatrix.
*/
// @VisibleForTesting
/*package*/ class Renderer implements GLSurfaceView.Renderer, TouchTracker.Listener {
/* package */ class Renderer
implements GLSurfaceView.Renderer, TouchTracker.Listener, OrientationListener.Listener {
private final SceneRenderer scene;
private final float[] projectionMatrix = new float[16];
@ -362,8 +291,9 @@ public final class SphericalSurfaceView extends GLSurfaceView {
}
/** Adjusts the GL camera's rotation based on device rotation. Runs on the sensor thread. */
@Override
@BinderThread
public synchronized void setDeviceOrientation(float[] matrix, float deviceRoll) {
public synchronized void onOrientationChange(float[] matrix, float deviceRoll) {
System.arraycopy(matrix, 0, deviceOrientationMatrix, 0, deviceOrientationMatrix.length);
this.deviceRoll = -deviceRoll;
updatePitchMatrix();

View File

@ -45,16 +45,16 @@ import android.view.View;
* Mesh as the user moves their finger. However, that requires quaternion interpolation.
*/
// @VisibleForTesting
/*package*/ class TouchTracker extends GestureDetector.SimpleOnGestureListener
implements View.OnTouchListener {
/* package */ class TouchTracker extends GestureDetector.SimpleOnGestureListener
implements View.OnTouchListener, OrientationListener.Listener {
/*package*/ interface Listener {
/* package */ interface Listener {
void onScrollChange(PointF scrollOffsetDegrees);
}
// Touch input won't change the pitch beyond +/- 45 degrees. This reduces awkward situations
// where the touch-based pitch and gyro-based pitch interact badly near the poles.
/*package*/ static final float MAX_PITCH_DEGREES = 45;
/* package */ static final float MAX_PITCH_DEGREES = 45;
// With every touch event, update the accumulated degrees offset by the new pixel amount.
private final PointF previousTouchPointPx = new PointF();
@ -132,8 +132,9 @@ import android.view.View;
return false;
}
@Override
@BinderThread
public void setRoll(float roll) {
public void onOrientationChange(float[] deviceOrientationMatrix, float roll) {
// We compensate for roll by rotating in the opposite direction.
this.roll = -roll;
}