diff --git a/code/mobile/android/PhoneVR/.gitignore b/code/mobile/android/PhoneVR/.gitignore index fa0251e2..706a185d 100644 --- a/code/mobile/android/PhoneVR/.gitignore +++ b/code/mobile/android/PhoneVR/.gitignore @@ -11,6 +11,7 @@ /ALVR /cardboard /gvr-android-sdk-1.200 +/arcore-android-sdk-main /app/libraries /app/libraries_gvr diff --git a/code/mobile/android/PhoneVR/app/CMakeLists.txt b/code/mobile/android/PhoneVR/app/CMakeLists.txt index 358126a1..c6014dae 100644 --- a/code/mobile/android/PhoneVR/app/CMakeLists.txt +++ b/code/mobile/android/PhoneVR/app/CMakeLists.txt @@ -19,6 +19,13 @@ add_library(native-lib-alvr SHARED ${LIB_SRC} ) +# Import the ARCore (Google Play Services for AR) library. +add_library(arcore SHARED IMPORTED) +set_target_properties(arcore PROPERTIES IMPORTED_LOCATION + ${ARCORE_LIBPATH}/${ANDROID_ABI}/libarcore_sdk_c.so + INTERFACE_INCLUDE_DIRECTORIES ${ARCORE_INCLUDE} +) + set(libs_dir ${CMAKE_CURRENT_SOURCE_DIR}/libraries) set(alvr_build_dir ${CMAKE_CURRENT_SOURCE_DIR}/../ALVR/build/alvr_client_core) @@ -31,10 +38,11 @@ target_include_directories(native-lib-alvr # ALVR Headers PUBLIC ${alvr_build_dir} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/../cardboard + PUBLIC ${ARCORE_INCLUDE} ) target_link_libraries(native-lib-alvr - log android EGL GLESv3 mediandk + log android EGL GLESv3 mediandk arcore ${alvr_build_dir}/${ANDROID_ABI}/libalvr_client_core.so ${libs_dir}/jni/${ANDROID_ABI}/libGfxPluginCardboard.so ) diff --git a/code/mobile/android/PhoneVR/app/build.gradle b/code/mobile/android/PhoneVR/app/build.gradle index 399bcf30..c3ca0810 100644 --- a/code/mobile/android/PhoneVR/app/build.gradle +++ b/code/mobile/android/PhoneVR/app/build.gradle @@ -7,6 +7,15 @@ apply plugin: 'com.google.gms.google-services' apply from: '../versioning.gradle' apply plugin: "com.diffplug.spotless" +/* +The arcore aar library contains the native shared libraries. These are +extracted before building to a temporary directory. + */ +def arcore_libpath = "${rootProject.layout.buildDirectory.get()}/arcore-native" + +// Create a configuration to mark which aars to extract .so files from +configurations { natives } + def keystorePropertiesFile = rootProject.file("keystore.properties") def keystoreProperties = new Properties() keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) @@ -50,6 +59,8 @@ android { externalNativeBuild { cmake { cppFlags "-std=c++17 -fexceptions -frtti" //-DANDROID_USE_LEGACY_TOOLCHAIN_FILE=ON" + arguments "-DARCORE_LIBPATH=${arcore_libpath}/jni", + "-DARCORE_INCLUDE=${project.rootDir}/arcore-android-sdk-main/libraries/include" } } @@ -174,6 +185,9 @@ dependencies { implementation 'androidx.camera:camera-lifecycle:1.3.4' implementation 'androidx.camera:camera-camera2:1.3.4' + implementation 'com.google.ar:core:1.46.0' + natives 'com.google.ar:core:1.46.0' + def acraVersion = '5.7.0' implementation "ch.acra:acra-mail:$acraVersion" implementation "ch.acra:acra-dialog:$acraVersion" @@ -199,6 +213,13 @@ task extractNdk(type: Copy) { include "jni/**/libgvr.so" } } + configurations.natives.files.each { f -> + copy { + from zipTree(f) + into arcore_libpath + include "jni/**/*" + } + } } task deleteNdk(type: Delete) { diff --git a/code/mobile/android/PhoneVR/app/src/gvr/CMakeLists.txt b/code/mobile/android/PhoneVR/app/src/gvr/CMakeLists.txt index 344e6b1b..10cf63d4 100644 --- a/code/mobile/android/PhoneVR/app/src/gvr/CMakeLists.txt +++ b/code/mobile/android/PhoneVR/app/src/gvr/CMakeLists.txt @@ -9,7 +9,6 @@ set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS} -O3 -std=c++17 -g") set(ANDROID_STL c++_static) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY BOTH) - set(gvr_libs_dir ${CMAKE_CURRENT_SOURCE_DIR}/../../libraries_gvr) file(GLOB_RECURSE LIB_SRC ../../../../../../common/libs/ifaddrs/*.c diff --git a/code/mobile/android/PhoneVR/app/src/main/AndroidManifest.xml b/code/mobile/android/PhoneVR/app/src/main/AndroidManifest.xml index c193327f..5a43f188 100644 --- a/code/mobile/android/PhoneVR/app/src/main/AndroidManifest.xml +++ b/code/mobile/android/PhoneVR/app/src/main/AndroidManifest.xml @@ -75,6 +75,8 @@ android:screenOrientation="landscape" android:theme="@style/Theme.AppCompat.NoActionBar" android:launchMode="singleTop" /> + + diff --git a/code/mobile/android/PhoneVR/app/src/main/cpp/alvr_main.cpp b/code/mobile/android/PhoneVR/app/src/main/cpp/alvr_main.cpp index c6336b01..aae6b297 100644 --- a/code/mobile/android/PhoneVR/app/src/main/cpp/alvr_main.cpp +++ b/code/mobile/android/PhoneVR/app/src/main/cpp/alvr_main.cpp @@ -1,6 +1,8 @@ #include "alvr_client_core.h" +#include "arcore_c_api.h" #include "cardboard.h" #include +#include #include #include #include @@ -17,6 +19,11 @@ using namespace nlohmann; uint64_t HEAD_ID = alvr_path_string_to_id("/user/head"); +// TODO: Make this configurable. +// Using ARCore orientation is more accurate, but causes a ~0.5 second delay, +// which is probably nauseating for most folks. TODO. +bool useARCoreOrientation = false; + // Note: the Cardboard SDK cannot estimate display time and an heuristic is used instead. const uint64_t VSYNC_QUEUE_INTERVAL_NS = 50e6; const float FLOOR_HEIGHT = 1.5; @@ -30,8 +37,18 @@ struct NativeContext { CardboardLensDistortion *lensDistortion = nullptr; CardboardDistortionRenderer *distortionRenderer = nullptr; + bool arcoreEnabled = false; + ArSession *arSession = nullptr; + ArFrame *arFrame = nullptr; + ArAnchor *arFloorAnchor = nullptr; + GLuint arTexture = 0; + + AlvrQuat lastOrientation = {0.f, 0.f, 0.f, 0.f}; + float lastPosition[3] = {0.f, 0.f, 0.f}; + int screenWidth = 0; int screenHeight = 0; + int screenRotation = 0; bool renderingParamsChanged = true; bool glContextRecreated = false; @@ -107,13 +124,161 @@ AlvrFov getFov(CardboardEye eye) { AlvrPose getPose(uint64_t timestampNs) { AlvrPose pose = {}; + bool returnLastPosition = false; + + if (!CTX.arcoreEnabled || (CTX.arcoreEnabled && !useARCoreOrientation)) { + float pos[3]; + float q[4]; + CardboardHeadTracker_getPose(CTX.headTracker, (int64_t) timestampNs, kLandscapeLeft, pos, q); + + auto inverseOrientation = AlvrQuat{q[0], q[1], q[2], q[3]}; + pose.orientation = inverseQuat(inverseOrientation); + CTX.lastOrientation = pose.orientation; + } + + if (CTX.arcoreEnabled && CTX.arSession != nullptr) { + if (eglGetCurrentContext() == EGL_NO_CONTEXT) { + throw std::runtime_error("Failed to get EGL context in getPose."); + returnLastPosition = false; + goto out; + } + + int ret = ArSession_update(CTX.arSession, CTX.arFrame); + if (ret != AR_SUCCESS) { + error("getPose: ArSession_update failed (%d), using last position", ret); + returnLastPosition = true; + goto out; + } + + ArCamera *arCamera = nullptr; + ArFrame_acquireCamera(CTX.arSession, CTX.arFrame, &arCamera); + + ArTrackingState arTrackingState; + ArCamera_getTrackingState(CTX.arSession, arCamera, &arTrackingState); + if (arTrackingState != AR_TRACKING_STATE_TRACKING) { + error("getPose: Camera is not tracking, using last position"); + if (arTrackingState == AR_TRACKING_STATE_PAUSED) { + error("- AR tracking state is PAUSED"); + ArTrackingFailureReason failureReason; + ArCamera_getTrackingFailureReason(CTX.arSession, arCamera, &failureReason); + error("- Failure reason: %d", failureReason); + } else if (arTrackingState == AR_TRACKING_STATE_STOPPED) { + error("- AR tracking state is STOPPED"); + } + returnLastPosition = true; + ArCamera_release(arCamera); + goto out; + } + + ArPose *arPose = nullptr; + ArPose_create(CTX.arSession, nullptr, &arPose); + ArCamera_getDisplayOrientedPose(CTX.arSession, arCamera, arPose); + // ArPose_getPoseRaw() returns a pose in {qx, qy, qz, qw, tx, ty, tz} format. + float arRawPose[7] = {0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f}; + ArPose_getPoseRaw(CTX.arSession, arPose, arRawPose); + + /* We determine floor position by finding the lowest detected plane. We do this by + * placing an anchor in the center position of the plane, and if it's lower than the + * currently placed anchor (CTX.floorAnchor), we replace it. + * + * (By default, ARCore's "world coordinates" space begins wherever the device is, but this + * can desync over time. Anchor position adapts to world space movement. */ + ArTrackableList *trackables = nullptr; + ArTrackableList_create(CTX.arSession, &trackables); + ArFrame_getUpdatedTrackables(CTX.arSession, CTX.arFrame, AR_TRACKABLE_PLANE, trackables); + int32_t detectedPlaneCount; + ArTrackableList_getSize(CTX.arSession, trackables, &detectedPlaneCount); + + for (int i = 0; i < detectedPlaneCount; i++) { + ArTrackable* arTrackable = nullptr; + ArTrackableList_acquireItem(CTX.arSession, trackables, i, + &arTrackable); + const ArPlane *plane = ArAsPlane(arTrackable); + ArTrackingState planeTrackingState; + ArTrackable_getTrackingState(CTX.arSession, arTrackable, &planeTrackingState); + ArPlaneType planeType; + ArPlane_getType(CTX.arSession, plane, &planeType); + if (planeTrackingState == AR_TRACKING_STATE_TRACKING && + planeType == AR_PLANE_HORIZONTAL_UPWARD_FACING) { + ArPose *planePose = nullptr; + ArPose_create(CTX.arSession, nullptr, &planePose); + ArPlane_getCenterPose(CTX.arSession, plane, planePose); + + bool reanchor = false; + if (CTX.arFloorAnchor == nullptr) { + reanchor = true; + } else { + ArPose *currentFloorPose = nullptr; + ArPose_create(CTX.arSession, nullptr, ¤tFloorPose); + + ArAnchor_getPose(CTX.arSession, CTX.arFloorAnchor, currentFloorPose); + float currentFloorPoseRaw[7] = {0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f}; + ArPose_getPoseRaw(CTX.arSession, currentFloorPose, currentFloorPoseRaw); + + float planePoseRaw[7] = {0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f}; + ArPose_getPoseRaw(CTX.arSession, planePose, planePoseRaw); + + if (planePoseRaw[5] < currentFloorPoseRaw[5]) { + info("Found new plane lower than pose (current %f vs new %f), reanchoring", + currentFloorPoseRaw[5], planePoseRaw[5]); + reanchor = true; + } + } + + if (reanchor) { + if (CTX.arFloorAnchor != nullptr) { + ArAnchor_detach(CTX.arSession, CTX.arFloorAnchor); + ArAnchor_release(CTX.arFloorAnchor); + } + + ArPose *planePoseNoRotation = ArPose_extractTranslation(CTX.arSession, + planePose); + ArTrackable_acquireNewAnchor(CTX.arSession, arTrackable, planePoseNoRotation, + &CTX.arFloorAnchor); + ArPose_destroy(planePoseNoRotation); + } + + ArPose_destroy(planePose); + } + } + + ArTrackableList_destroy(trackables); + + float anchorRawPose[7] = {0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f}; + if (CTX.arFloorAnchor != nullptr) { + ArPose *anchorPose = nullptr; + ArPose_create(CTX.arSession, nullptr, &anchorPose); + ArAnchor_getPose(CTX.arSession, CTX.arFloorAnchor, anchorPose); + ArPose_getPoseRaw(CTX.arSession, anchorPose, anchorRawPose); + info("anchor pose %f %f %f %f %f %f %f", anchorRawPose[0], anchorRawPose[1], anchorRawPose[2], anchorRawPose[3], anchorRawPose[4], anchorRawPose[5], anchorRawPose[6]); + } + + pose.position[0] = arRawPose[4]; + pose.position[1] = arRawPose[5] - anchorRawPose[5]; + pose.position[2] = arRawPose[6]; - float pos[3]; - float q[4]; - CardboardHeadTracker_getPose(CTX.headTracker, (int64_t) timestampNs, kLandscapeLeft, pos, q); + for (int i = 0; i < 3; i++) { + CTX.lastPosition[i] = arRawPose[i + 4]; + } - auto inverseOrientation = AlvrQuat{q[0], q[1], q[2], q[3]}; - pose.orientation = inverseQuat(inverseOrientation); + if (useARCoreOrientation) { + auto orientation = AlvrQuat{arRawPose[0], arRawPose[1], arRawPose[2], + arRawPose[3]}; + pose.orientation = orientation; + CTX.lastOrientation = pose.orientation; + } + + ArPose_destroy(arPose); + ArCamera_release(arCamera); + } + +out: + if (returnLastPosition) { + pose.orientation = CTX.lastOrientation; + for (int i = 0; i < 3; i++) { + pose.position[i] = CTX.lastPosition[i]; + } + } return pose; } @@ -142,6 +307,70 @@ void updateViewConfigs(uint64_t targetTimestampNs = 0) { void inputThread() { auto deadline = std::chrono::steady_clock::now(); + if (CTX.arcoreEnabled) { + /* ARCore requires an EGL context to work. Since we're calling it from a secondary + * thread that is not the main GL thread, we need to provide our own context. */ + info("inputThread: creating ARCore EGL context for input thread"); + // 1. Initialize EGL + EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); + if (display == EGL_NO_DISPLAY) { + throw std::runtime_error("Failed to get EGL display."); + } + + if (!eglInitialize(display, nullptr, nullptr)) { + throw std::runtime_error("Failed to initialize EGL."); + } + + // 2. Choose EGL configuration + EGLint numConfigs; + EGLConfig config; + EGLint configAttribs[] = { + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, + EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, + EGL_BLUE_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_RED_SIZE, 8, + EGL_ALPHA_SIZE, 8, + EGL_NONE + }; + + if (!eglChooseConfig(display, configAttribs, &config, 1, &numConfigs)) { + throw std::runtime_error("Failed to choose EGL config."); + } + + if (numConfigs == 0) { + throw std::runtime_error("No suitable EGL configurations found."); + } + + // 3. Create an offscreen (pbuffer) surface + EGLint pbufferAttribs[] = { + EGL_WIDTH, 1920, + EGL_HEIGHT, 1920, + EGL_NONE + }; + + EGLSurface surface = eglCreatePbufferSurface(display, config, pbufferAttribs); + if (surface == EGL_NO_SURFACE) { + throw std::runtime_error("Failed to create EGL pbuffer surface."); + } + + // 4. Create an EGL context + EGLint contextAttribs[] = { + EGL_CONTEXT_CLIENT_VERSION, 3, // OpenGL ES 3.0 context + EGL_NONE + }; + + EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttribs); + if (context == EGL_NO_CONTEXT) { + throw std::runtime_error("Failed to create EGL context."); + } + + // 5. Bind the context to the current thread + if (!eglMakeCurrent(display, surface, surface, context)) { + throw std::runtime_error("Failed to make EGL context current."); + } + } + info("inputThread: thread staring..."); while (CTX.streaming) { @@ -162,7 +391,7 @@ extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *) { } extern "C" JNIEXPORT void JNICALL Java_viritualisres_phonevr_ALVRActivity_initializeNative( - JNIEnv *env, jobject obj, jint screenWidth, jint screenHeight, jfloat refreshRate) { + JNIEnv *env, jobject obj, jint screenWidth, jint screenHeight, jfloat refreshRate, jboolean enableARCore) { CTX.javaContext = env->NewGlobalRef(obj); uint32_t viewWidth = std::max(screenWidth, screenHeight) / 2; @@ -188,6 +417,37 @@ extern "C" JNIEXPORT void JNICALL Java_viritualisres_phonevr_ALVRActivity_initia Cardboard_initializeAndroid(CTX.javaVm, CTX.javaContext); CTX.headTracker = CardboardHeadTracker_create(); + + CTX.arcoreEnabled = (bool) enableARCore; + if (CTX.arcoreEnabled) { + if (ArSession_create(env, CTX.javaContext, &CTX.arSession) != AR_SUCCESS) { + error("initializeNative: Could not create ARCore session"); + CTX.arcoreEnabled = false; + return; + } + + ArConfig* arConfig = nullptr; + ArConfig_create(CTX.arSession, &arConfig); + + // Explicitly disable all unnecessary features to preserve CPU power. + ArConfig_setDepthMode(CTX.arSession, arConfig, AR_DEPTH_MODE_DISABLED); + ArConfig_setLightEstimationMode(CTX.arSession, arConfig, AR_LIGHT_ESTIMATION_MODE_DISABLED); + ArConfig_setPlaneFindingMode(CTX.arSession, arConfig, AR_PLANE_FINDING_MODE_HORIZONTAL_AND_VERTICAL); + ArConfig_setCloudAnchorMode(CTX.arSession, arConfig, AR_CLOUD_ANCHOR_MODE_DISABLED); + + // Set "latest camera image" update mode (ArSession_update returns immediately without blocking) + ArConfig_setUpdateMode(CTX.arSession, arConfig, AR_UPDATE_MODE_LATEST_CAMERA_IMAGE); + + // TODO: Add camera config filter: + // https://developers.google.com/ar/develop/c/camera-configs + + if (ArSession_configure(CTX.arSession, arConfig) != AR_SUCCESS) { + error("initializeNative: Could not configure ARCore session"); + return; + } + + ArFrame_create(CTX.arSession, &CTX.arFrame); + } } extern "C" JNIEXPORT void JNICALL Java_viritualisres_phonevr_ALVRActivity_destroyNative(JNIEnv *, @@ -201,11 +461,25 @@ extern "C" JNIEXPORT void JNICALL Java_viritualisres_phonevr_ALVRActivity_destro CTX.lensDistortion = nullptr; CardboardDistortionRenderer_destroy(CTX.distortionRenderer); CTX.distortionRenderer = nullptr; + + if (CTX.arcoreEnabled && CTX.arSession != nullptr) { + ArSession_destroy(CTX.arSession); + CTX.arSession = nullptr; + + ArFrame_destroy(CTX.arFrame); + CTX.arFrame = nullptr; + } } extern "C" JNIEXPORT void JNICALL Java_viritualisres_phonevr_ALVRActivity_resumeNative(JNIEnv *, jobject) { CardboardHeadTracker_resume(CTX.headTracker); + if (CTX.arcoreEnabled && CTX.arSession != nullptr) { + ArStatus arSessionStatus = ArSession_resume(CTX.arSession); + if (arSessionStatus != AR_SUCCESS) { + error("Failed to resume tracking: %d", arSessionStatus); + } + } CTX.renderingParamsChanged = true; @@ -248,6 +522,12 @@ extern "C" JNIEXPORT void JNICALL Java_viritualisres_phonevr_ALVRActivity_setScr CTX.renderingParamsChanged = true; } +extern "C" JNIEXPORT void JNICALL Java_viritualisres_phonevr_ALVRActivity_setScreenRotationNative( + JNIEnv *, jobject, jint rotation) { + CTX.screenRotation = rotation; + CTX.renderingParamsChanged = true; +} + extern "C" JNIEXPORT void JNICALL Java_viritualisres_phonevr_ALVRActivity_sendBatteryLevel( JNIEnv *, jobject, jfloat level, jboolean plugged) { alvr_send_battery(HEAD_ID, level, plugged); @@ -301,6 +581,11 @@ extern "C" JNIEXPORT void JNICALL Java_viritualisres_phonevr_ALVRActivity_render CTX.fovArr[kLeft] = getFov(kLeft); CTX.fovArr[kRight] = getFov(kRight); + if (CTX.arcoreEnabled && CTX.arSession != nullptr) { + ArSession_setDisplayGeometry( + CTX.arSession, CTX.screenRotation, CTX.screenWidth, CTX.screenHeight); + } + info("renderingParamsChanged, updating new view configs (FOV) to alvr"); // alvr_send_views_config(fovArr, CTX.eyeOffsets[0] - CTX.eyeOffsets[1]); } @@ -341,6 +626,20 @@ extern "C" JNIEXPORT void JNICALL Java_viritualisres_phonevr_ALVRActivity_render (uint32_t *) &CTX.lobbyTextures[1]}; alvr_resume_opengl(CTX.screenWidth / 2, CTX.screenHeight, targetViews, 1, true); + if (CTX.arcoreEnabled && CTX.arSession != nullptr) { + GLuint arTextureIdArray[1]; + glGenTextures(1, arTextureIdArray); + CTX.arTexture = arTextureIdArray[0]; + + GL(glBindTexture(GL_TEXTURE_2D, CTX.arTexture)); + GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)); + GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)); + GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)); + GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)); + + ArSession_setCameraTextureName(CTX.arSession, CTX.arTexture); + } + CTX.renderingParamsChanged = false; CTX.glContextRecreated = false; } @@ -508,7 +807,7 @@ extern "C" JNIEXPORT void JNICALL Java_viritualisres_phonevr_ALVRActivity_render // offset head pos to Eye Position offsetPosWithQuat(pose.orientation, headToEye, viewInputs[eye].pose.position); - viewInputs[eye].pose.orientation = pose.orientation; + viewInputs[eye].pose = pose; viewInputs[eye].fov = getFov((CardboardEye) eye); viewInputs[eye].swapchain_index = 0; } diff --git a/code/mobile/android/PhoneVR/app/src/main/cpp/utils.h b/code/mobile/android/PhoneVR/app/src/main/cpp/utils.h index bc31f22b..3f1d5c63 100644 --- a/code/mobile/android/PhoneVR/app/src/main/cpp/utils.h +++ b/code/mobile/android/PhoneVR/app/src/main/cpp/utils.h @@ -2,6 +2,7 @@ #define PHONEVR_UTILS_H #include "alvr_client_core.h" +#include "arcore_c_api.h" #include #include #include @@ -12,7 +13,7 @@ const char LOG_TAG[] = "ALVR_PVR_NATIVE"; -void log(AlvrLogLevel level, +static void log(AlvrLogLevel level, const char *FILE_NAME, unsigned int LINE_NO, const char *FUNC, @@ -87,4 +88,22 @@ static const char *GlErrorString(GLenum error) { func; \ GLCheckErrors(__FILE__, __LINE__) +/* Returns a pose having the translation of this pose but no rotation. + * The caller is responsible for freeing the original pose and the output pose. + * (C++ port of Pose.extractTranslation.) */ +static ArPose *ArPose_extractTranslation(ArSession *session, ArPose *in_pose) { + float raw_pose[7] = {0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f}; + ArPose_getPoseRaw(session, in_pose, raw_pose); + + raw_pose[0] = 0.f; + raw_pose[1] = 0.f; + raw_pose[2] = 0.f; + raw_pose[3] = 0.f; + + ArPose *out_pose = nullptr; + ArPose_create(session, raw_pose, &out_pose); + + return out_pose; +} + #endif // PHONEVR_UTILS_H diff --git a/code/mobile/android/PhoneVR/app/src/main/java/viritualisres/phonevr/ALVRActivity.java b/code/mobile/android/PhoneVR/app/src/main/java/viritualisres/phonevr/ALVRActivity.java index b5b09f56..b48000d3 100644 --- a/code/mobile/android/PhoneVR/app/src/main/java/viritualisres/phonevr/ALVRActivity.java +++ b/code/mobile/android/PhoneVR/app/src/main/java/viritualisres/phonevr/ALVRActivity.java @@ -27,6 +27,9 @@ import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; +import com.google.ar.core.ArCoreApk; +import com.google.ar.core.exceptions.UnavailableDeviceNotCompatibleException; +import com.google.ar.core.exceptions.UnavailableUserDeclinedInstallationException; import java.util.Objects; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; @@ -40,6 +43,8 @@ public class ALVRActivity extends AppCompatActivity private static final String TAG = ALVRActivity.class.getSimpleName() + "-Java"; + private boolean surfaceChanged = false; + // Permission request codes private static final int PERMISSIONS_REQUEST_CODE = 2; @@ -105,7 +110,8 @@ public void onCreate(Bundle savedInstance) { float refreshRate = display.getRefreshRate(); Log.i(TAG, "Refresh rate: " + refreshRate); - initializeNative(width, height, refreshRate); + // TODO: config option for ARCore + initializeNative(width, height, refreshRate, true); setContentView(R.layout.activity_vr); glView = findViewById(R.id.surface_view); @@ -163,6 +169,16 @@ protected void onResume() { return; } + // TODO check if we actually want ARCore + try { + ArCoreApk.getInstance().requestInstall(this, true); + } catch (UnavailableDeviceNotCompatibleException e) { + Toast.makeText(this, R.string.arcore_not_supported, Toast.LENGTH_LONG).show(); + // TODO disable ARCore toggle + } catch (UnavailableUserDeclinedInstallationException e) { + // TODO disable ARCore toggle + } + glView.onResume(); resumeNative(); bMonitor.startMonitoring(this); @@ -192,10 +208,15 @@ public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) { @Override public void onSurfaceChanged(GL10 gl10, int width, int height) { setScreenResolutionNative(width, height); + surfaceChanged = true; } @Override public void onDrawFrame(GL10 gl10) { + if (surfaceChanged) { + setScreenRotationNative(getWindowManager().getDefaultDisplay().getRotation()); + surfaceChanged = false; + } renderNative(); } } @@ -285,7 +306,7 @@ private void setImmersiveSticky() { } private native void initializeNative( - int screenWidth, int screenHeight, float screenRefreshRate); + int screenWidth, int screenHeight, float screenRefreshRate, boolean enableARCore); private native void destroyNative(); @@ -297,6 +318,8 @@ private native void initializeNative( private native void setScreenResolutionNative(int width, int height); + private native void setScreenRotationNative(int displayRotation); + private native void renderNative(); private native void switchViewerNative(); diff --git a/code/mobile/android/PhoneVR/app/src/main/res/values/strings.xml b/code/mobile/android/PhoneVR/app/src/main/res/values/strings.xml index ce5664f9..574d09cd 100644 --- a/code/mobile/android/PhoneVR/app/src/main/res/values/strings.xml +++ b/code/mobile/android/PhoneVR/app/src/main/res/values/strings.xml @@ -42,4 +42,5 @@ Switch PhoneVR Server Remember choice Max Brightness + Your device does not support ARCore; position tracking has been disabled diff --git a/code/mobile/android/PhoneVR/prepare-alvr-deps.sh b/code/mobile/android/PhoneVR/prepare-alvr-deps.sh index 524bb187..e81246bb 100644 --- a/code/mobile/android/PhoneVR/prepare-alvr-deps.sh +++ b/code/mobile/android/PhoneVR/prepare-alvr-deps.sh @@ -51,3 +51,7 @@ unzip download.zip rm download.zip fi +rm -r "arcore-android-sdk" +curl -sLS https://github.com/google-ar/arcore-android-sdk/archive/refs/heads/main.zip > download.zip +unzip download.zip +rm download.zip \ No newline at end of file