diff --git a/build.gradle b/build.gradle index 2fa2ebc7..285af7ea 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { } dependencies { classpath 'com.android.tools.build:gradle:8.13.1' - classpath 'com.google.protobuf:protobuf-gradle-plugin:0.9.4' + classpath 'com.google.protobuf:protobuf-gradle-plugin:0.10.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d4081da4..aaaabb3c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/hellocardboard-android/CMakeLists.txt b/hellocardboard-android/CMakeLists.txt index ce183446..de02ac11 100644 --- a/hellocardboard-android/CMakeLists.txt +++ b/hellocardboard-android/CMakeLists.txt @@ -45,3 +45,6 @@ target_link_libraries(cardboard_jni ${GLESv3_LIB} ${LOG_LIB} ${CARDBOARD_LIB}) + +target_link_options(cardboard_jni PRIVATE "-Wl,-z,max-page-size=16384") + diff --git a/hellocardboard-android/build.gradle b/hellocardboard-android/build.gradle index afc87147..716c872a 100644 --- a/hellocardboard-android/build.gradle +++ b/hellocardboard-android/build.gradle @@ -18,7 +18,7 @@ android { minSdkVersion = 26 targetSdkVersion = 35 versionCode 1 - versionName "1.33.0" + versionName "1.34.0" testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' ndk { abiFilters 'armeabi-v7a', 'arm64-v8a' @@ -37,6 +37,11 @@ android { path "CMakeLists.txt" } namespace = 'com.google.cardboard' + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } } dependencies { @@ -52,8 +57,9 @@ dependencies { // The dependencies for NDK builds live inside the .aar files so they need to // be extracted before NDK targets can link against. -task extractNdk(type: Copy) { - if (file("${project.rootDir}/sdk/build/outputs/aar/sdk-release.aar").exists()) { +task extractNdk { + dependsOn ':sdk:assembleRelease' + doLast { copy { from zipTree("${project.rootDir}/sdk/build/outputs/aar/sdk-release.aar") into "libraries/" @@ -71,5 +77,5 @@ task deleteNdk(type: Delete) { delete "libraries/cardboard.h" } -build.dependsOn(extractNdk) +preBuild.dependsOn(extractNdk) clean.dependsOn(deleteNdk) diff --git a/hellocardboard-ios/HelloCardboard-Info.plist b/hellocardboard-ios/HelloCardboard-Info.plist index 360da51c..6daa9683 100644 --- a/hellocardboard-ios/HelloCardboard-Info.plist +++ b/hellocardboard-ios/HelloCardboard-Info.plist @@ -15,9 +15,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.33.0 + 1.34.0 CFBundleVersion - 1.33.0 + 1.34.0 LSRequiresIPhoneOS NSCameraUsageDescription diff --git a/java_api/proguard-rules.pro b/java_api/proguard-rules.pro new file mode 100644 index 00000000..83d01a9f --- /dev/null +++ b/java_api/proguard-rules.pro @@ -0,0 +1,6 @@ +# Proguard rules to preserve Cardboard OSS as a dependency. +# +# Because of native to and from Java calls, Java code is preserved. +# TODO(b/266823260): Remove forked ProGuard file. +-keep class com.google.cardboard.sdk.** { *; } +-keep class com.google.cardboard.proto.** { *; } \ No newline at end of file diff --git a/java_api/src/main/AndroidManifest.xml b/java_api/src/main/AndroidManifest.xml new file mode 100644 index 00000000..659bfcee --- /dev/null +++ b/java_api/src/main/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/java_api/src/main/java/com/google/cardboard/sdk/CardboardGLSurfaceView.java b/java_api/src/main/java/com/google/cardboard/sdk/CardboardGLSurfaceView.java new file mode 100644 index 00000000..92505194 --- /dev/null +++ b/java_api/src/main/java/com/google/cardboard/sdk/CardboardGLSurfaceView.java @@ -0,0 +1,2127 @@ +/* + * Copyright (C) 2008 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.cardboard.sdk; + +import android.content.Context; +import android.opengl.EGL14; +import android.opengl.EGLExt; +import android.opengl.GLDebugHelper; +import android.opengl.GLSurfaceView.EGLConfigChooser; +import android.opengl.GLSurfaceView.EGLContextFactory; +import android.opengl.GLSurfaceView.EGLWindowSurfaceFactory; +import android.opengl.GLSurfaceView.Renderer; +import android.os.Build; +import android.util.AttributeSet; +import android.util.Log; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import java.io.Writer; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGL11; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; +import javax.microedition.khronos.egl.EGLSurface; +import javax.microedition.khronos.opengles.GL; +import javax.microedition.khronos.opengles.GL10; + +/** + * A port to CardboardOSS of Gvr's surface, to compare against GLSurfaceView. + * Local changes to Gvr's version, on top of the listed below to the GLSurfaceView fork: + * - Removal of EglReadyListener instance member and all code related to it. + * Reasoning: it allowed to native code to reuse the GL context in use within Java code and it was + * no longer needed. + * - Removal of EglReadyListener.Listener interface implementation in the GlThread. + * Reasoning: it worked in tandem with the removed instance of EglReadyListener and it was no + * longer needed. + * + * A port of Android's GLSurfaceView, to be used by VR applications. + * + * /////////////////////////// ATTENTION /////////////////////////// + * Latest GLSurfaceView Change-id: 3054d010327f8888919d7e0a97d63773dd015681. + * + * Other than whitespace, we purposefully follow Android's style conventions, not the + * Google3 style guide, in order to make syncs easier. + * + * Local changes to GLSurfaceView include: + * - Delinting whitespace using git5 fix. + * - Removing SystemProperties checks to confirm the code is running with GLES20 support. + * - Copying helper functions from EGLLogWrapper to format error messages. + * - Replacing trace functions with their app-space, API compatible equivalents. + * - Fixing several lint errors for final modifiers and variable naming. + * - Cherry pick of changes from change-id I2bb5b475433ebff8ca82db385e228fef11e32e20 + * that removes support for pre-Android-3.0 GL driver limitations. + * - Remove global locking behavior (only necessary for pre-Android-3.0 functionality). + * - Add support to preserve the GL thread onDetachedFromWindow. + * - Expose requestExitAndWait() to allow explicit request for the GL thread to exit. + * - Expose isDetachedFromWindow() to subclasses. + * - Listen to events from EglReadyListener to allow for context sharing. + * - New function onPause(Runnable). The Runnable will be executed after the last render call, + * but before the rendering thread is paused. + * - New function onSurfaceDestroyed(Runnable). Handles surfaceDestroyed and executes the Runnable + * before the surface and GL context are destroyed. + * + * An implementation of SurfaceView that uses the dedicated surface for + * displaying OpenGL rendering. + *

+ * A CardboardGLSurfaceView provides the following features: + *

+ *

+ * + *
+ *

Developer Guides

+ *

For more information about how to use OpenGL, read the + * OpenGL developer guide.

+ *
+ * + *

Using CardboardGLSurfaceView

+ *

+ * Typically you use CardboardGLSurfaceView by subclassing it and overriding one or more of the + * View system input event methods. If your application does not need to override event + * methods then CardboardGLSurfaceView can be used as-is. For the most part + * CardboardGLSurfaceView behavior is customized by calling "set" methods rather than by subclassing. + * For example, unlike a regular View, drawing is delegated to a separate Renderer object which + * is registered with the CardboardGLSurfaceView + * using the {@link #setRenderer(Renderer)} call. + *

+ *

Initializing CardboardGLSurfaceView

+ * All you have to do to initialize a CardboardGLSurfaceView is call {@link #setRenderer(Renderer)}. + * However, if desired, you can modify the default behavior of CardboardGLSurfaceView by calling one or + * more of these methods before calling setRenderer: + * + *

+ *

Specifying the android.view.Surface

+ * By default CardboardGLSurfaceView will create a PixelFormat.RGB_888 format surface. If a translucent + * surface is required, call getHolder().setFormat(PixelFormat.TRANSLUCENT). + * The exact format of a TRANSLUCENT surface is device dependent, but it will be + * a 32-bit-per-pixel surface with 8 bits per component. + *

+ *

Choosing an EGL Configuration

+ * A given Android device may support multiple EGLConfig rendering configurations. + * The available configurations may differ in how may channels of data are present, as + * well as how many bits are allocated to each channel. Therefore, the first thing + * CardboardGLSurfaceView has to do when starting to render is choose what EGLConfig to use. + *

+ * By default CardboardGLSurfaceView chooses a EGLConfig that has an RGB_888 pixel format, + * with at least a 16-bit depth buffer and no stencil. + *

+ * If you would prefer a different EGLConfig + * you can override the default behavior by calling one of the + * setEGLConfigChooser methods. + *

+ *

Debug Behavior

+ * You can optionally modify the behavior of CardboardGLSurfaceView by calling + * one or more of the debugging methods {@link #setDebugFlags(int)}, + * and {@link #setGLWrapper}. These methods may be called before and/or after setRenderer, but + * typically they are called before setRenderer so that they take effect immediately. + *

+ *

Setting a Renderer

+ * Finally, you must call {@link #setRenderer} to register a {@link Renderer}. + * The renderer is + * responsible for doing the actual OpenGL rendering. + *

+ *

Rendering Mode

+ * Once the renderer is set, you can control whether the renderer draws + * continuously or on-demand by calling +* {@link #setRenderMode}. The default is continuous rendering. + *

+ *

Activity Life-cycle

+ * A CardboardGLSurfaceView must be notified when the activity is paused and resumed. CardboardGLSurfaceView clients + * are required to call {@link #onPause()} when the activity pauses and + * {@link #onResume()} when the activity resumes. These calls allow CardboardGLSurfaceView to + * pause and resume the rendering thread, and also allow CardboardGLSurfaceView to release and recreate + * the OpenGL display. + *

+ *

Handling events

+ *

+ * To handle an event you will typically subclass CardboardGLSurfaceView and override the + * appropriate method, just as you would with any other View. However, when handling + * the event, you may need to communicate with the Renderer object + * that's running in the rendering thread. You can do this using any + * standard Java cross-thread communication mechanism. In addition, + * one relatively easy way to communicate with your renderer is + * to call + * {@link #queueEvent(Runnable)}. For example: + *

+ * class MyCardboardGLSurfaceView extends CardboardGLSurfaceView {
+ *
+ *     private MyRenderer mMyRenderer;
+ *
+ *     public void start() {
+ *         mMyRenderer = ...;
+ *         setRenderer(mMyRenderer);
+ *     }
+ *
+ *     public boolean onKeyDown(int keyCode, KeyEvent event) {
+ *         if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
+ *             queueEvent(new Runnable() {
+ *                 // This method will be called on the rendering
+ *                 // thread:
+ *                 public void run() {
+ *                     mMyRenderer.handleDpadCenter();
+ *                 }});
+ *             return true;
+ *         }
+ *         return super.onKeyDown(keyCode, event);
+ *     }
+ * }
+ * 
+ * + * @hide + */ +public class CardboardGLSurfaceView extends SurfaceView implements SurfaceHolder.Callback2 { + private static final String TAG = "CardboardGLSurfaceView"; + private static final boolean LOG_ATTACH_DETACH = false; + private static final boolean LOG_THREADS = false; + private static final boolean LOG_PAUSE_RESUME = false; + private static final boolean LOG_SURFACE = false; + private static final boolean LOG_RENDERER = false; + private static final boolean LOG_RENDERER_DRAW_FRAME = false; + private static final boolean LOG_EGL = false; + // Context flag that says whether the context has error reporting disabled. + // https://www.khronos.org/registry/OpenGL/extensions/KHR/KHR_no_error.txt + private static final int GL_CONTEXT_FLAG_NO_ERROR_BIT_KHR = 0x8; + + /** + * The renderer only renders + * when the surface is created, or when {@link #requestRender} is called. + * + * @see #getRenderMode() + * @see #setRenderMode(int) + * @see #requestRender() + */ + public static final int RENDERMODE_WHEN_DIRTY = 0; + /** + * The renderer is called + * continuously to re-render the scene. + * + * @see #getRenderMode() + * @see #setRenderMode(int) + */ + public static final int RENDERMODE_CONTINUOUSLY = 1; + + /** + * The EGL context communicates through a normal swap chain. Multiple + * buffers are made available to the application, and eglSwapBuffers() + * is called by this view at the end of every frame draw. + * + * This is the default swap mode. + * + * @see setSwapMode(int) + */ + public static final int SWAPMODE_QUEUED = 0; + + /** + * The EGL context is placed in single buffer mode, where the application + * and the display concurrently access the same graphical buffer. eglSwapBuffers() + * is called once, the first frame that is drawn after the swap mode is changed, + * and then not called again. Applications are expected to call glFlush() manually + * to commit their draw commands. + * + * @see setSwapMode(int) + */ + public static final int SWAPMODE_SINGLE = 1; + + /** + * The EGL context communicates through a normal swap chain, but eglSwapBuffers() + * is not called on behalf of the application. This allows the application to + * execute onDrawFrame(), without the view assuming that a frame will be submitted. + * + * @see setSwapMode(int) + */ + public static final int SWAPMODE_MANUAL = 2; + + /** + * Check glError() after every GL call and throw an exception if glError indicates + * that an error has occurred. This can be used to help track down which OpenGL ES call + * is causing an error. + * + * @see #getDebugFlags + * @see #setDebugFlags + */ + public static final int DEBUG_CHECK_GL_ERROR = 1; + + /** + * Log GL calls to the system log at "verbose" level with tag "CardboardGLSurfaceView". + * + * @see #getDebugFlags + * @see #setDebugFlags + */ + public static final int DEBUG_LOG_GL_CALLS = 2; + + /** + * Standard View constructor. In order to render something, you + * must call {@link #setRenderer} to register a renderer. + */ + public CardboardGLSurfaceView(Context context) { + super(context); + init(); + } + + /** + * Standard View constructor. In order to render something, you + * must call {@link #setRenderer} to register a renderer. + */ + public CardboardGLSurfaceView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + @Override + protected void finalize() throws Throwable { + try { + if (mGLThread != null) { + // GLThread may still be running if this view was never + // attached to a window. + mGLThread.requestExitAndWait(); + } + } finally { + super.finalize(); + } + } + + private void init() { + // Install a SurfaceHolder.Callback so we get notified when the + // underlying surface is created and destroyed + SurfaceHolder holder = getHolder(); + holder.addCallback(this); + // setFormat is done by SurfaceView in SDK 2.3 and newer. Uncomment + // this statement if back-porting to 2.2 or older: + // holder.setFormat(PixelFormat.RGB_565); + // + // setType is not needed for SDK 2.0 or newer. Uncomment this + // statement if back-porting this code to older SDKs. + // holder.setType(SurfaceHolder.SURFACE_TYPE_GPU); + } + + /** + * Set the glWrapper. If the glWrapper is not null, its + * {@link GLWrapper#wrap(GL)} method is called + * whenever a surface is created. A GLWrapper can be used to wrap + * the GL object that's passed to the renderer. Wrapping a GL + * object enables examining and modifying the behavior of the + * GL calls made by the renderer. + *

+ * Wrapping is typically used for debugging purposes. + *

+ * The default value is null. + * @param glWrapper the new GLWrapper + */ + public void setGLWrapper(GLWrapper glWrapper) { + mGLWrapper = glWrapper; + } + + /** + * Set the debug flags to a new value. The value is + * constructed by OR-together zero or more + * of the DEBUG_CHECK_* constants. The debug flags take effect + * whenever a surface is created. The default value is zero. + * @param debugFlags the new debug flags + * @see #DEBUG_CHECK_GL_ERROR + * @see #DEBUG_LOG_GL_CALLS + */ + public void setDebugFlags(int debugFlags) { + mDebugFlags = debugFlags; + } + + /** + * Get the current value of the debug flags. + * @return the current value of the debug flags. + */ + public int getDebugFlags() { + return mDebugFlags; + } + + /** + * Control whether the EGL context is preserved when the CardboardGLSurfaceView is paused and + * resumed. + *

+ * If set to true, then the EGL context may be preserved when the CardboardGLSurfaceView is paused. + * Whether the EGL context is actually preserved or not depends upon whether the + * Android device that the program is running on can support an arbitrary number of EGL + * contexts or not. Devices that can only support a limited number of EGL contexts must + * release the EGL context in order to allow multiple applications to share the GPU. + *

+ * If set to false, the EGL context will be released when the CardboardGLSurfaceView is paused, + * and recreated when the CardboardGLSurfaceView is resumed. + *

+ * + * The default is false. + * + * @param preserveOnPause preserve the EGL context when paused + */ + public void setPreserveEGLContextOnPause(boolean preserveOnPause) { + mPreserveEGLContextOnPause = preserveOnPause; + } + + /** + * @return true if the EGL context will be preserved when paused + */ + public boolean getPreserveEGLContextOnPause() { + return mPreserveEGLContextOnPause; + } + + /** + * Set the renderer associated with this view. Also starts the thread that + * will call the renderer, which in turn causes the rendering to start. + *

This method should be called once and only once in the life-cycle of + * a CardboardGLSurfaceView. + *

The following CardboardGLSurfaceView methods can only be called before + * setRenderer is called: + *

+ *

+ * The following CardboardGLSurfaceView methods can only be called after + * setRenderer is called: + *

+ * + * @param renderer the renderer to use to perform OpenGL drawing. + */ + public void setRenderer(Renderer renderer) { + checkRenderThreadState(); + if (mEGLConfigChooser == null) { + mEGLConfigChooser = new SimpleEGLConfigChooser(true); + } + if (mEGLContextFactory == null) { + mEGLContextFactory = new DefaultContextFactory(); + } + if (mEGLWindowSurfaceFactory == null) { + mEGLWindowSurfaceFactory = new DefaultWindowSurfaceFactory(); + } + mRenderer = renderer; + mGLThread = new GLThread(mThisWeakRef); + mGLThread.start(); + } + + /** + * Install a custom EGLContextFactory. + *

If this method is + * called, it must be called before {@link #setRenderer(Renderer)} + * is called. + *

+ * If this method is not called, then by default + * a context will be created with no shared context and + * with a null attribute list. + */ + public void setEGLContextFactory(EGLContextFactory factory) { + checkRenderThreadState(); + mEGLContextFactory = factory; + } + + /** + * Install a custom EGLWindowSurfaceFactory. + *

If this method is + * called, it must be called before {@link #setRenderer(Renderer)} + * is called. + *

+ * If this method is not called, then by default + * a window surface will be created with a null attribute list. + */ + public void setEGLWindowSurfaceFactory(EGLWindowSurfaceFactory factory) { + checkRenderThreadState(); + mEGLWindowSurfaceFactory = factory; + } + + /** + * Install a custom EGLConfigChooser. + *

If this method is + * called, it must be called before {@link #setRenderer(Renderer)} + * is called. + *

+ * If no setEGLConfigChooser method is called, then by default the + * view will choose an EGLConfig that is compatible with the current + * android.view.Surface, with a depth buffer depth of + * at least 16 bits. + * @param configChooser + */ + public void setEGLConfigChooser(EGLConfigChooser configChooser) { + checkRenderThreadState(); + mEGLConfigChooser = configChooser; + } + + /** + * Install a config chooser which will choose a config + * as close to 16-bit RGB as possible, with or without an optional depth + * buffer as close to 16-bits as possible. + *

If this method is + * called, it must be called before {@link #setRenderer(Renderer)} + * is called. + *

+ * If no setEGLConfigChooser method is called, then by default the + * view will choose an RGB_888 surface with a depth buffer depth of + * at least 16 bits. + * + * @param needDepth + */ + public void setEGLConfigChooser(boolean needDepth) { + setEGLConfigChooser(new SimpleEGLConfigChooser(needDepth)); + } + + /** + * Install a config chooser which will choose a config + * with at least the specified depthSize and stencilSize, + * and exactly the specified redSize, greenSize, blueSize and alphaSize. + *

If this method is + * called, it must be called before {@link #setRenderer(Renderer)} + * is called. + *

+ * If no setEGLConfigChooser method is called, then by default the + * view will choose an RGB_888 surface with a depth buffer depth of + * at least 16 bits. + * + */ + public void setEGLConfigChooser( + int redSize, int greenSize, int blueSize, int alphaSize, int depthSize, int stencilSize) { + setEGLConfigChooser( + new ComponentSizeChooser(redSize, greenSize, blueSize, alphaSize, depthSize, stencilSize)); + } + + /** + * Inform the default EGLContextFactory and default EGLConfigChooser + * which EGLContext client version to pick. + *

Use this method to create an OpenGL ES 2.0-compatible context. + * Example: + *

+   *     public MyView(Context context) {
+   *         super(context);
+   *         setEGLContextClientVersion(2); // Pick an OpenGL ES 2.0 context.
+   *         setRenderer(new MyRenderer());
+   *     }
+   * 
+ *

Note: Activities which require OpenGL ES 2.0 should indicate this by + * setting @lt;uses-feature android:glEsVersion="0x00020000" /> in the activity's + * AndroidManifest.xml file. + *

If this method is called, it must be called before {@link #setRenderer(Renderer)} + * is called. + *

This method only affects the behavior of the default EGLContexFactory and the + * default EGLConfigChooser. If + * {@link #setEGLContextFactory(EGLContextFactory)} has been called, then the supplied + * EGLContextFactory is responsible for creating an OpenGL ES 2.0-compatible context. + * If + * {@link #setEGLConfigChooser(EGLConfigChooser)} has been called, then the supplied + * EGLConfigChooser is responsible for choosing an OpenGL ES 2.0-compatible config. + * @param version The EGLContext client version to choose. Use 2 for OpenGL ES 2.0 + */ + public void setEGLContextClientVersion(int version) { + checkRenderThreadState(); + mEGLContextClientVersion = version; + } + + /** + * Set the rendering mode. When renderMode is + * RENDERMODE_CONTINUOUSLY, the renderer is called + * repeatedly to re-render the scene. When renderMode + * is RENDERMODE_WHEN_DIRTY, the renderer only rendered when the surface + * is created, or when {@link #requestRender} is called. Defaults to RENDERMODE_CONTINUOUSLY. + *

+ * Using RENDERMODE_WHEN_DIRTY can improve battery life and overall system performance + * by allowing the GPU and CPU to idle when the view does not need to be updated. + *

+ * This method can only be called after {@link #setRenderer(Renderer)} + * + * @param renderMode one of the RENDERMODE_X constants + * @see #RENDERMODE_CONTINUOUSLY + * @see #RENDERMODE_WHEN_DIRTY + */ + public void setRenderMode(int renderMode) { + mGLThread.setRenderMode(renderMode); + } + + /** + * Get the current rendering mode. May be called + * from any thread. Must not be called before a renderer has been set. + * @return the current rendering mode. + * @see #RENDERMODE_CONTINUOUSLY + * @see #RENDERMODE_WHEN_DIRTY + */ + public int getRenderMode() { + return mGLThread.getRenderMode(); + } + + /** + * Request that the renderer render a frame. + * This method is typically used when the render mode has been set to + * {@link #RENDERMODE_WHEN_DIRTY}, so that frames are only rendered on demand. + * May be called + * from any thread. Must not be called before a renderer has been set. + */ + public void requestRender() { + mGLThread.requestRender(); + } + + /** + * Set the swap mode. + * + *

When swapMode is SWAPMODE_QUEUED, the view behaves as the default GLSurfaceView + * implementation -- frames are buffered, and eglSwapBuffers() is automatically executed after + * each call to Renderer.onDrawFrame(). + * + *

When swapMode is SWAPMODE_SINGLE, the application and the display concurrently access the + * same graphical buffer, eglSwapBuffers() is not called continuously, and applications are + * expected to synchronize with the display, and to submit their draw commands manually by calling + * glFlush() or glFinish(). + * + *

When swapMode is SWAPMODE_MANUAL, frames are buffered, but eglSwapBuffers() is not called. + * Applications are expected to call it themselves. + * + *

Note that SWAPMODE_SINGLE requires two extensions to function properly -- + * EGL_KHR_mutable_render_buffer and EGL_ANDROID_front_buffer_auto_refresh. + * EGL_KHR_mutable_render_buffer, in particular, requires bits to be set in the EGL config during + * EGL context construction. So applications that specify an EGLContextFactory will need to + * initialize the context appropriately. With the correct extensions and configurations, the + * behavior of this extension is undefined. + * + *

This method can only be called after {@link #setRenderer(Renderer)} + * + * @param swapMode one of the SWAPMODE_X constants + * @see #SWAPMODE_QUEUED + * @see #SWAPMODE_SINGLE + * @see #SWAPMODE_MANUAL + */ + public void setSwapMode(int swapMode) { + if (swapMode == SWAPMODE_SINGLE + && Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) { + Log.e( + TAG, + "setSwapMode(SWAPMODE_SINGLE) requires Jellybean MR1 (EGL14 dependency)."); + return; + } + mGLThread.setSwapMode(swapMode); + } + + /** + * Set whether to preserve the GL thread when {@link #onDetachedFromWindow()} is called. + *

Defaults to false. + *

If this method is called, it must be called before + * {@link #setRenderer(Renderer)} is called. + */ + public void setPreserveGlThreadOnDetachedFromWindow(boolean preserveGlThread) { + checkRenderThreadState(); + mPreserveGlThreadOnDetachedFromWindow = preserveGlThread; + } + + /** + * This method is part of the SurfaceHolder.Callback interface, and is + * not normally called or subclassed by clients of CardboardGLSurfaceView. + */ + @Override + public void surfaceCreated(SurfaceHolder holder) { + mGLThread.surfaceCreated(); + } + + /** + * This method is part of the SurfaceHolder.Callback interface, and is + * not normally called or subclassed by clients of CardboardGLSurfaceView. + */ + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + // Surface will be destroyed when we return + onSurfaceDestroyed(null); + } + + /** + * Handles destruction of the target surface of this CardboardGLSurfaceView and executes an event just + * before the surface is destroyed. This is useful for cleanup and prevents an extremely rare race + * condition where a render call is made after an event added with {@link queueEvent} is + * processed, but before the super call is made. Both the passed Runnable and the required state + * changes on the rendering thread are completed before this function returns. + * @param r Runnable to execute just before destroying the surface. There will be no onDraw() + * callbacks between this Runnable's execution and surface destruction. Can be null. + */ + public void onSurfaceDestroyed(Runnable r) { + mGLThread.surfaceDestroyed(r); + } + + /** + * This method is part of the SurfaceHolder.Callback interface, and is + * not normally called or subclassed by clients of CardboardGLSurfaceView. + */ + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { + mGLThread.onWindowResize(w, h); + } + + /** + * This method is part of the SurfaceHolder.Callback interface, and is + * not normally called or subclassed by clients of CardboardGLSurfaceView. + */ + @Override + public void surfaceRedrawNeeded(SurfaceHolder holder) { + mGLThread.requestRenderAndWait(); + } + + /** + * Inform the view that the activity is paused. The owner of this view must + * call this method when the activity is paused. Calling this method will + * pause the rendering thread. + * Must not be called before a renderer has been set. + */ + public void onPause() { + mGLThread.onPause(null); + } + + /** + * Same as {@link #onPause()}, but executes a runnable just before pausing the GL thread. This + * prevents an extremely rare race condition where the GL thread executes a render call between + * queueEvent() and onPause(). Both the passed Runnable and the required state changes on the GL + * thread are completed before this function returns. + * @param r Runnable to execute just before pausing the thread. There will be no onDraw() + * callbacks between this Runnable's execution and the thread being paused. Can be null. + */ + public void onPause(Runnable r) { + mGLThread.onPause(r); + } + + /** + * Inform the view that the activity is resumed. The owner of this view must + * call this method when the activity is resumed. Calling this method will + * recreate the OpenGL display and resume the rendering + * thread. + * Must not be called before a renderer has been set. + */ + public void onResume() { + mGLThread.onResume(); + } + + /** + * Queue a runnable to be run on the GL rendering thread. + * + *

This can be used to communicate with the Renderer on the rendering thread. Must not be + * called before a renderer has been set. + * + *

Most methods that interact with the GL thread, such as {@link #onPause()}, + * {@link #onResume()}, {@link #surfaceDestroyed}, etc. are event barriers, i.e., all events + * queued before these calls are executed before the GL thread state change is performed. This is + * enforced by synchronizing on mGLThreadManager and processing all events before applying any + * state changes in the GL thread render loop. + * + * @param r the runnable to be run on the GL rendering thread. + */ + public void queueEvent(Runnable r) { + mGLThread.queueEvent(r); + } + + /** + * Requests and waits for the GL thread to exit. + * Must not be called on the GL thread. + */ + public void requestExitAndWait() { + mGLThread.requestExitAndWait(); + } + + /** + * This method is used as part of the View class and is not normally + * called or subclassed by clients of CardboardGLSurfaceView. + */ + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (LOG_ATTACH_DETACH) { + Log.d(TAG, "onAttachedToWindow reattach =" + mDetached); + } + if (mDetached && (mRenderer != null) && !mPreserveGlThreadOnDetachedFromWindow) { + int renderMode = RENDERMODE_CONTINUOUSLY; + int swapMode = SWAPMODE_QUEUED; + if (mGLThread != null) { + renderMode = mGLThread.getRenderMode(); + swapMode = mGLThread.getSwapMode(); + } + mGLThread = new GLThread(mThisWeakRef); + if (renderMode != RENDERMODE_CONTINUOUSLY) { + mGLThread.setRenderMode(renderMode); + } + if (swapMode != SWAPMODE_QUEUED) { + mGLThread.setSwapMode(swapMode); + } + mGLThread.start(); + } + mDetached = false; + } + + @Override + protected void onDetachedFromWindow() { + if (LOG_ATTACH_DETACH) { + Log.d(TAG, "onDetachedFromWindow"); + } + if (mGLThread != null && !mPreserveGlThreadOnDetachedFromWindow) { + requestExitAndWait(); + } + mDetached = true; + super.onDetachedFromWindow(); + } + + /** + * Whether the view has received an onDetachedFromWindow() callback. + * + *

Note: this can only be called from the UI thread. + */ + protected boolean isDetachedFromWindow() { + return mDetached; + } + + // ---------------------------------------------------------------------- + + /** + * An interface used to wrap a GL interface. + *

Typically + * used for implementing debugging and tracing on top of the default + * GL interface. You would typically use this by creating your own class + * that implemented all the GL methods by delegating to another GL instance. + * Then you could add your own behavior before or after calling the + * delegate. All the GLWrapper would do was instantiate and return the + * wrapper GL instance: + *

+   * class MyGLWrapper implements GLWrapper {
+   *     GL wrap(GL gl) {
+   *         return new MyGLImplementation(gl);
+   *     }
+   *     static class MyGLImplementation implements GL,GL10,GL11,... {
+   *         ...
+   *     }
+   * }
+   * 
+ * @see #setGLWrapper(GLWrapper) + */ + public interface GLWrapper { + /** + * Wraps a gl interface in another gl interface. + * @param gl a GL interface that is to be wrapped. + * @return either the input argument or another GL object that wraps the input argument. + */ + GL wrap(GL gl); + } + + private class DefaultContextFactory implements EGLContextFactory { + private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098; + private static final String LOG_TAG = TAG + "::DefaultContextFactory"; + + @Override + public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig config) { + int[] attribList = {EGL_CONTEXT_CLIENT_VERSION, mEGLContextClientVersion, EGL10.EGL_NONE}; + + return egl.eglCreateContext( + display, + config, + EGL10.EGL_NO_CONTEXT, + mEGLContextClientVersion != 0 ? attribList : null); + } + + @Override + public void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context) { + if (!egl.eglDestroyContext(display, context)) { + Log.e("DefaultContextFactory", "display:" + display + " context: " + context); + if (LOG_THREADS) { + Log.i(LOG_TAG, "tid=" + Thread.currentThread().getId()); + } + EglHelper.throwEglException("eglDestroyContext", egl.eglGetError()); + } + } + } + + private static class DefaultWindowSurfaceFactory implements EGLWindowSurfaceFactory { + private static final String LOG_TAG = TAG + "::DefaultWindowSurfaceFactory"; + + @Override + public EGLSurface createWindowSurface( + EGL10 egl, EGLDisplay display, EGLConfig config, Object nativeWindow) { + EGLSurface result = null; + try { + result = egl.eglCreateWindowSurface(display, config, nativeWindow, null); + } catch (IllegalArgumentException e) { + // This exception indicates that the surface flinger surface + // is not valid. This can happen if the surface flinger surface has + // been torn down, but the application has not yet been + // notified via SurfaceHolder.Callback.surfaceDestroyed. + // In theory the application should be notified first, + // but in practice sometimes it is not. See b/4588890 + Log.e(LOG_TAG, "eglCreateWindowSurface", e); + } + return result; + } + + @Override + public void destroySurface(EGL10 egl, EGLDisplay display, EGLSurface surface) { + egl.eglDestroySurface(display, surface); + } + } + + private abstract class BaseConfigChooser implements EGLConfigChooser { + private static final String LOG_TAG = TAG + "::BaseConfigChooser"; + + public BaseConfigChooser(int[] configSpec) { + mConfigSpec = filterConfigSpec(configSpec); + } + + @Override + public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) { + int[] numConfig = new int[1]; + if (!egl.eglChooseConfig(display, mConfigSpec, null, 0, numConfig)) { + for (int i = 1; i < mConfigSpec.length; ++i) { + if (mConfigSpec[i - 1] == EGL10.EGL_RENDERABLE_TYPE + && mConfigSpec[i] == EGLExt.EGL_OPENGL_ES3_BIT_KHR) { + Log.w(LOG_TAG, "Failed to choose GLES 3 config, will try 2."); + mConfigSpec[i] = EGL14.EGL_OPENGL_ES2_BIT; + return chooseConfig(egl, display); + } + } + throw new IllegalArgumentException("eglChooseConfig failed"); + } + + int numConfigs = numConfig[0]; + + if (numConfigs <= 0) { + throw new IllegalArgumentException("No configs match configSpec"); + } + + EGLConfig[] configs = new EGLConfig[numConfigs]; + if (!egl.eglChooseConfig(display, mConfigSpec, configs, numConfigs, numConfig)) { + throw new IllegalArgumentException("eglChooseConfig#2 failed"); + } + EGLConfig config = chooseConfig(egl, display, configs); + if (config == null) { + throw new IllegalArgumentException("No config chosen"); + } + return config; + } + + abstract EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, EGLConfig[] configs); + + protected int[] mConfigSpec; + + private int[] filterConfigSpec(int[] configSpec) { + if (mEGLContextClientVersion != 2 && mEGLContextClientVersion != 3) { + return configSpec; + } + /* We know none of the subclasses define EGL_RENDERABLE_TYPE. + * And we know the configSpec is well formed. + */ + int len = configSpec.length; + int[] newConfigSpec = new int[len + 2]; + System.arraycopy(configSpec, 0, newConfigSpec, 0, len - 1); + newConfigSpec[len - 1] = EGL10.EGL_RENDERABLE_TYPE; + if (mEGLContextClientVersion == 2) { + newConfigSpec[len] = EGL14.EGL_OPENGL_ES2_BIT; /* EGL_OPENGL_ES2_BIT */ + } else { + newConfigSpec[len] = EGLExt.EGL_OPENGL_ES3_BIT_KHR; /* EGL_OPENGL_ES3_BIT_KHR */ + } + newConfigSpec[len + 1] = EGL10.EGL_NONE; + return newConfigSpec; + } + } + + /** + * Choose a configuration with exactly the specified r,g,b,a sizes, + * and at least the specified depth and stencil sizes. + */ + private class ComponentSizeChooser extends BaseConfigChooser { + public ComponentSizeChooser( + int redSize, int greenSize, int blueSize, int alphaSize, int depthSize, int stencilSize) { + super( + new int[] { + EGL10.EGL_RED_SIZE, + redSize, + EGL10.EGL_GREEN_SIZE, + greenSize, + EGL10.EGL_BLUE_SIZE, + blueSize, + EGL10.EGL_ALPHA_SIZE, + alphaSize, + EGL10.EGL_DEPTH_SIZE, + depthSize, + EGL10.EGL_STENCIL_SIZE, + stencilSize, + EGL10.EGL_NONE + }); + mValue = new int[1]; + mRedSize = redSize; + mGreenSize = greenSize; + mBlueSize = blueSize; + mAlphaSize = alphaSize; + mDepthSize = depthSize; + mStencilSize = stencilSize; + } + + @Override + public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, EGLConfig[] configs) { + for (EGLConfig config : configs) { + int d = findConfigAttrib(egl, display, config, EGL10.EGL_DEPTH_SIZE, 0); + int s = findConfigAttrib(egl, display, config, EGL10.EGL_STENCIL_SIZE, 0); + if ((d >= mDepthSize) && (s >= mStencilSize)) { + int r = findConfigAttrib(egl, display, config, EGL10.EGL_RED_SIZE, 0); + int g = findConfigAttrib(egl, display, config, EGL10.EGL_GREEN_SIZE, 0); + int b = findConfigAttrib(egl, display, config, EGL10.EGL_BLUE_SIZE, 0); + int a = findConfigAttrib(egl, display, config, EGL10.EGL_ALPHA_SIZE, 0); + if ((r == mRedSize) && (g == mGreenSize) && (b == mBlueSize) && (a == mAlphaSize)) { + return config; + } + } + } + return null; + } + + private int findConfigAttrib( + EGL10 egl, EGLDisplay display, EGLConfig config, int attribute, int defaultValue) { + + if (egl.eglGetConfigAttrib(display, config, attribute, mValue)) { + return mValue[0]; + } + return defaultValue; + } + + private int[] mValue; + // Subclasses can adjust these values: + protected int mRedSize; + protected int mGreenSize; + protected int mBlueSize; + protected int mAlphaSize; + protected int mDepthSize; + protected int mStencilSize; + } + + /** + * This class will choose a RGB_888 surface with + * or without a depth buffer. + * + */ + private class SimpleEGLConfigChooser extends ComponentSizeChooser { + public SimpleEGLConfigChooser(boolean withDepthBuffer) { + super(8, 8, 8, 0, withDepthBuffer ? 16 : 0, 0); + } + } + + /** + * An EGL helper class. + */ + private static class EglHelper { + private static final String LOG_TAG = TAG + "::EglHelper"; + + // This constant is defined in EGL_ANDROID_front_buffer_auto_refresh, an + // Android-specific EGL extension. + public static final int EGL_FRONT_BUFFER_AUTO_REFRESH = 0x314C; + + public EglHelper(WeakReference cardboardGLSurfaceViewWeakRef) { + mCardboardGLSurfaceViewWeakRef = cardboardGLSurfaceViewWeakRef; + } + + public void start() { + if (LOG_EGL) { + Log.w(LOG_TAG, "start() tid=" + Thread.currentThread().getId()); + } + if (mEgl == null) { + initialize(); + } + // Destroy the previous EGL context if it exists, so that we can release the old share group + // and create a new one with the recently-created app context. + // TODO(b/37944681): Create automated test that exercises this condition. + if (mEglContext != null) { + mEgl.eglDestroyContext(mEglDisplay, mEglContext); + mEglContext = null; + } + + if (mPendingEglContext == null) { + // TODO(b/37944681): Create automated test that exercises this condition. + createPendingEglContext(); + } + mEglContext = mPendingEglContext; + mEglDisplay = mPendingEglDisplay; + mPendingEglContext = null; + mPendingEglDisplay = null; + } + + // Creates a new context, shared with the app context if necessary, and stashes it. + // The new context is considered "pending" until the run loop has a chance to grab it. + public void renewPendingEglContext() { + if (mEgl == null) { + initialize(); + } + // If a pending EDS context already exists, destroy and recreate it to ensure that we're + // in a share group with the most recently created application context. + // TODO(b/37944681): Create automated test that exercises this condition. + if (mPendingEglContext != null) { + mEgl.eglDestroyContext(mEglDisplay, mPendingEglContext); + } + createPendingEglContext(); + } + + private void initialize() { + /* + * Get an EGL instance + */ + mEgl = (EGL10) EGLContext.getEGL(); + + /* + * Get to the default display. + */ + mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); + + if (mEglDisplay == EGL10.EGL_NO_DISPLAY) { + throw new RuntimeException("eglGetDisplay failed"); + } + + /* + * We can now initialize EGL for that display + */ + int[] version = new int[2]; + if (!mEgl.eglInitialize(mEglDisplay, version)) { + throw new RuntimeException("eglInitialize failed"); + } + mEglSurface = null; + } + + // Chooses and stashes the latest EGL config, creates a new EGL context (in the app's share + // group if necessary) and returns a new context. + private void createPendingEglContext() { + // Obtain the current display and ensure that it has been initialized. + mPendingEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); + if (mPendingEglDisplay == EGL10.EGL_NO_DISPLAY) { + throw new RuntimeException("eglGetDisplay failed"); + } + int[] version = new int[2]; + if (!mEgl.eglInitialize(mPendingEglDisplay, version)) { + throw new RuntimeException("eglInitialize failed"); + } + // The surface view provides the config chooser and context factory. + EGLContext context; + CardboardGLSurfaceView view = mCardboardGLSurfaceViewWeakRef.get(); + if (view == null) { + mEglConfig = null; + context = null; + } else { + mEglConfig = view.mEGLConfigChooser.chooseConfig(mEgl, mPendingEglDisplay); + context = view.mEGLContextFactory.createContext( + mEgl, mPendingEglDisplay, mEglConfig); + } + if (context == null || context == EGL10.EGL_NO_CONTEXT) { + context = null; + // Android does not provide us a way to be notified when an EGL context becomes invalid, + // it only provides lifetime events for the Surface. For the best robustness, we handle + // EGL_BAD_CONTEXT gracefully by clearing out the invalid context, then waiting for the app + // to renew it. + int err = mEgl.eglGetError(); + if (err == EGL10.EGL_BAD_CONTEXT) { + Log.e( + LOG_TAG, + "Stashed EGL context has become invalid and can no longer be used for sharing."); + mPendingEglContext = null; + mPendingEglDisplay = null; + mEglConfig = null; + return; + } else { + throwEglException("createPendingEglContext", err); + } + } + if (LOG_EGL) { + Log.w( + LOG_TAG, + "createPendingEglContext " + context + " tid=" + Thread.currentThread().getId()); + } + mPendingEglContext = context; + } + + /** + * Create an egl surface for the current SurfaceHolder surface. If a surface + * already exists, destroy it before creating the new surface. + * + * @return true if the surface was created successfully. + */ + public boolean createSurface() { + if (LOG_EGL) { + Log.w(LOG_TAG, "createSurface() tid=" + Thread.currentThread().getId()); + } + /* + * Check preconditions. + */ + if (mEgl == null) { + throw new RuntimeException("egl not initialized"); + } + if (mEglDisplay == null) { + throw new RuntimeException("eglDisplay not initialized"); + } + if (mEglConfig == null) { + throw new RuntimeException("mEglConfig not initialized"); + } + + /* + * The window size has changed, so we need to create a new + * surface. + */ + destroySurfaceImp(); + + /* + * Create an EGL surface we can render into. + */ + CardboardGLSurfaceView view = mCardboardGLSurfaceViewWeakRef.get(); + if (view != null) { + mEglSurface = + view.mEGLWindowSurfaceFactory.createWindowSurface( + mEgl, mEglDisplay, mEglConfig, view.getHolder()); + } else { + mEglSurface = null; + } + + if (mEglSurface == null || mEglSurface == EGL10.EGL_NO_SURFACE) { + int error = mEgl.eglGetError(); + if (error == EGL10.EGL_BAD_NATIVE_WINDOW) { + Log.e(LOG_TAG, "createWindowSurface returned EGL_BAD_NATIVE_WINDOW."); + } + return false; + } + + /* + * Before we can issue GL commands, we need to make sure + * the context is current and bound to a surface. + */ + if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) { + /* + * Could not make the context current, probably because the underlying + * SurfaceView surface has been destroyed. + */ + logEglErrorAsWarning(LOG_TAG, "eglMakeCurrent", mEgl.eglGetError()); + return false; + } + + return true; + } + + /** + * Create a GL object for the current EGL context. + * @return + */ + GL createGL() { + + GL gl = mEglContext.getGL(); + CardboardGLSurfaceView view = mCardboardGLSurfaceViewWeakRef.get(); + if (view != null) { + if (view.mGLWrapper != null) { + gl = view.mGLWrapper.wrap(gl); + } + + if ((view.mDebugFlags & (DEBUG_CHECK_GL_ERROR | DEBUG_LOG_GL_CALLS)) != 0) { + int configFlags = 0; + Writer log = null; + if ((view.mDebugFlags & DEBUG_CHECK_GL_ERROR) != 0) { + configFlags |= GLDebugHelper.CONFIG_CHECK_GL_ERROR; + } + if ((view.mDebugFlags & DEBUG_LOG_GL_CALLS) != 0) { + log = new LogWriter(); + } + gl = GLDebugHelper.wrap(gl, configFlags, log); + } + } + return gl; + } + + /** + * Display the current render surface. + * @return the EGL error code from eglSwapBuffers. + */ + public int swap() { + if (!mEgl.eglSwapBuffers(mEglDisplay, mEglSurface)) { + return mEgl.eglGetError(); + } + return EGL10.EGL_SUCCESS; + } + + public void destroySurface() { + if (LOG_EGL) { + Log.w(LOG_TAG, "destroySurface() tid=" + Thread.currentThread().getId()); + } + destroySurfaceImp(); + } + + public void setEglSurfaceAttrib(int attribute, int value) { + if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) { + Log.e(LOG_TAG, "Cannot call eglSurfaceAttrib. API version is too low."); + return; + } + + // TODO(adamgousetis): Is there a way to convert from EGL10 to EGL14? + android.opengl.EGLDisplay display = EGL14.eglGetCurrentDisplay(); + android.opengl.EGLSurface surface = EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW); + boolean success = EGL14.eglSurfaceAttrib(display, surface, attribute, value); + if (!success) { + Log.e(LOG_TAG, "eglSurfaceAttrib() failed. attribute=" + attribute + " value=" + value); + } + } + + private void destroySurfaceImp() { + if (mEglSurface != null && mEglSurface != EGL10.EGL_NO_SURFACE) { + mEgl.eglMakeCurrent( + mEglDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT); + CardboardGLSurfaceView view = mCardboardGLSurfaceViewWeakRef.get(); + if (view != null) { + view.mEGLWindowSurfaceFactory.destroySurface(mEgl, mEglDisplay, mEglSurface); + } + mEglSurface = null; + } + } + + public void finish() { + if (LOG_EGL) { + Log.w(LOG_TAG, "finish() tid=" + Thread.currentThread().getId()); + } + if (mEglContext != null) { + CardboardGLSurfaceView view = mCardboardGLSurfaceViewWeakRef.get(); + if (view != null) { + view.mEGLContextFactory.destroyContext(mEgl, mEglDisplay, mEglContext); + } + mEglContext = null; + } + if (mEglDisplay != null) { + mEgl.eglTerminate(mEglDisplay); + mEglDisplay = null; + } + } + + private void throwEglException(String function) { + throwEglException(function, mEgl.eglGetError()); + } + + public static void throwEglException(String function, int error) { + String message = formatEglError(function, error); + if (LOG_THREADS) { + Log.e( + LOG_TAG, + "throwEglException tid=" + Thread.currentThread().getId() + " " + message); + } + throw new RuntimeException(message); + } + + public static void logEglErrorAsWarning(String tag, String function, int error) { + Log.w(tag, formatEglError(function, error)); + } + + public static String formatEglError(String function, int error) { + return function + " failed: " + getErrorString(error); + } + + // This function was copied from EGLLogWrapper + private static String getHex(int value) { + return "0x" + Integer.toHexString(value); + } + + // This function was copied from EGLLogWrapper + private static String getErrorString(int error) { + switch (error) { + case EGL11.EGL_SUCCESS: + return "EGL_SUCCESS"; + case EGL11.EGL_NOT_INITIALIZED: + return "EGL_NOT_INITIALIZED"; + case EGL11.EGL_BAD_ACCESS: + return "EGL_BAD_ACCESS"; + case EGL11.EGL_BAD_ALLOC: + return "EGL_BAD_ALLOC"; + case EGL11.EGL_BAD_ATTRIBUTE: + return "EGL_BAD_ATTRIBUTE"; + case EGL11.EGL_BAD_CONFIG: + return "EGL_BAD_CONFIG"; + case EGL11.EGL_BAD_CONTEXT: + return "EGL_BAD_CONTEXT"; + case EGL11.EGL_BAD_CURRENT_SURFACE: + return "EGL_BAD_CURRENT_SURFACE"; + case EGL11.EGL_BAD_DISPLAY: + return "EGL_BAD_DISPLAY"; + case EGL11.EGL_BAD_MATCH: + return "EGL_BAD_MATCH"; + case EGL11.EGL_BAD_NATIVE_PIXMAP: + return "EGL_BAD_NATIVE_PIXMAP"; + case EGL11.EGL_BAD_NATIVE_WINDOW: + return "EGL_BAD_NATIVE_WINDOW"; + case EGL11.EGL_BAD_PARAMETER: + return "EGL_BAD_PARAMETER"; + case EGL11.EGL_BAD_SURFACE: + return "EGL_BAD_SURFACE"; + case EGL11.EGL_CONTEXT_LOST: + return "EGL_CONTEXT_LOST"; + default: + return getHex(error); + } + } + + private WeakReference mCardboardGLSurfaceViewWeakRef; + EGL10 mEgl; + EGLDisplay mEglDisplay; + EGLSurface mEglSurface; + EGLConfig mEglConfig; + EGLContext mEglContext; + EGLContext mPendingEglContext; + EGLDisplay mPendingEglDisplay; + } + + /** + * A generic GL Thread. Takes care of initializing EGL and GL. Delegates + * to a Renderer instance to do the actual drawing. Can be configured to + * render continuously or on request. + * + * All potentially blocking synchronization is done through the + * mGLThreadManager object. This avoids multiple-lock ordering issues. + * + */ + static class GLThread extends Thread { + private static final String LOG_TAG = TAG + "::GLThread"; + private static final String LOG_MAIN_THREAD_TAG = TAG + "::MainThread"; + + GLThread(WeakReference cardboardGLSurfaceViewWeakRef) { + super(); + mWidth = 0; + mHeight = 0; + mRequestRender = true; + mRenderMode = RENDERMODE_CONTINUOUSLY; + mRequestedSwapMode = SWAPMODE_QUEUED; + mWantRenderNotification = false; + mCardboardGLSurfaceViewWeakRef = cardboardGLSurfaceViewWeakRef; + } + + @Override + public void run() { + setName("GLThread " + getId()); + if (LOG_THREADS) { + Log.i(LOG_TAG, "starting tid=" + getId()); + } + + try { + guardedRun(); + } catch (InterruptedException e) { + // fall thru and exit normally + } finally { + mGLThreadManager.threadExiting(this); + } + } + + /* + * This private method should only be called inside a + * synchronized(mGLThreadManager) block. + */ + private void stopEglSurfaceLocked() { + if (mHaveEglSurface) { + mHaveEglSurface = false; + mEglHelper.destroySurface(); + } + } + + /* + * This private method should only be called inside a + * synchronized(mGLThreadManager) block. + */ + private void stopEglContextLocked() { + if (mHaveEglContext) { + mEglHelper.finish(); + mHaveEglContext = false; + mGLThreadManager.releaseEglContextLocked(this); + } + } + + private void guardedRun() throws InterruptedException { + mEglHelper = new EglHelper(mCardboardGLSurfaceViewWeakRef); + mHaveEglContext = false; + mHaveEglSurface = false; + mWantRenderNotification = false; + + try { + GL10 gl = null; + boolean createEglContext = false; + boolean createEglSurface = false; + boolean createGlInterface = false; + boolean lostEglContext = false; + boolean sizeChanged = false; + boolean wantRenderNotification = false; + boolean doRenderNotification = false; + boolean askedToReleaseEglContext = false; + boolean applySwapMode = false; + int swapMode = SWAPMODE_QUEUED; + int w = 0; + int h = 0; + Runnable event = null; + + while (true) { + synchronized (mGLThreadManager) { + while (true) { + if (mShouldExit) { + return; + } + + if (!mEventQueue.isEmpty()) { + event = mEventQueue.remove(0); + break; + } + + // Update the pause state. + boolean pausing = false; + if (mPaused != mRequestPaused) { + pausing = mRequestPaused; + mPaused = mRequestPaused; + mGLThreadManager.notifyAll(); + if (LOG_PAUSE_RESUME) { + Log.i(LOG_TAG, "mPaused is now " + mPaused + " tid=" + getId()); + } + } + + // Do we need to give up the EGL context? + if (mShouldReleaseEglContext) { + if (LOG_SURFACE) { + Log.i(LOG_TAG, "releasing EGL context because asked to tid=" + getId()); + } + stopEglSurfaceLocked(); + stopEglContextLocked(); + mShouldReleaseEglContext = false; + askedToReleaseEglContext = true; + } + + // Have we lost the EGL context? + if (lostEglContext) { + stopEglSurfaceLocked(); + stopEglContextLocked(); + lostEglContext = false; + } + + // When pausing, release the EGL surface: + if (pausing && mHaveEglSurface) { + if (LOG_SURFACE) { + Log.i(LOG_TAG, "releasing EGL surface because paused tid=" + getId()); + } + stopEglSurfaceLocked(); + } + + // When pausing, optionally release the EGL Context: + if (pausing && mHaveEglContext) { + CardboardGLSurfaceView view = mCardboardGLSurfaceViewWeakRef.get(); + boolean preserveEglContextOnPause = + view == null ? false : view.mPreserveEGLContextOnPause; + if (!preserveEglContextOnPause) { + stopEglContextLocked(); + if (LOG_SURFACE) { + Log.i(LOG_TAG, "releasing EGL context because paused tid=" + getId()); + } + } + } + + // Have we lost the SurfaceView surface? + if ((!mHasSurface) && (!mWaitingForSurface)) { + if (LOG_SURFACE) { + Log.i(LOG_TAG, "noticed surfaceView surface lost tid=" + getId()); + } + if (mHaveEglSurface) { + stopEglSurfaceLocked(); + } + mWaitingForSurface = true; + mSurfaceIsBad = false; + mGLThreadManager.notifyAll(); + } + + // Have we acquired the surface view surface? + if (mHasSurface && mWaitingForSurface) { + if (LOG_SURFACE) { + Log.i(LOG_TAG, "noticed surfaceView surface acquired tid=" + getId()); + } + mWaitingForSurface = false; + mGLThreadManager.notifyAll(); + } + + if (doRenderNotification) { + if (LOG_SURFACE) { + Log.i(LOG_TAG, "sending render notification tid=" + getId()); + } + mWantRenderNotification = false; + doRenderNotification = false; + mRenderComplete = true; + mGLThreadManager.notifyAll(); + } + + // Ready to draw? + if (readyToDraw()) { + + // If we don't have an EGL context, try to acquire one. + if (!mHaveEglContext) { + if (askedToReleaseEglContext) { + askedToReleaseEglContext = false; + } else { + try { + mEglHelper.start(); + } catch (RuntimeException t) { + mGLThreadManager.releaseEglContextLocked(this); + throw t; + } + if (mEglHelper.mEglContext != null) { + mHaveEglContext = true; + createEglContext = true; + mGLThreadManager.notifyAll(); + } + } + } + + if (mHaveEglContext && !mHaveEglSurface) { + mHaveEglSurface = true; + createEglSurface = true; + createGlInterface = true; + sizeChanged = true; + } + + if (mHaveEglSurface) { + if (mSizeChanged) { + sizeChanged = true; + w = mWidth; + h = mHeight; + mWantRenderNotification = true; + if (LOG_SURFACE) { + Log.i(LOG_TAG, "noticing that we want render notification tid=" + getId()); + } + + // Destroy and recreate the EGL surface. + createEglSurface = true; + + mSizeChanged = false; + } + mRequestRender = false; + mGLThreadManager.notifyAll(); + if (mWantRenderNotification) { + wantRenderNotification = true; + } + applySwapMode = mRequestedSwapMode != swapMode; + swapMode = mRequestedSwapMode; + break; + } + } + + // By design, this is the only place in a GLThread thread where we wait(). + if (LOG_THREADS) { + Log.i( + LOG_TAG, + "waiting tid=" + + getId() + + " mHaveEglContext: " + + mHaveEglContext + + " mHaveEglSurface: " + + mHaveEglSurface + + " mFinishedCreatingEglSurface: " + + mFinishedCreatingEglSurface + + " mPaused: " + + mPaused + + " mHasSurface: " + + mHasSurface + + " mSurfaceIsBad: " + + mSurfaceIsBad + + " mWaitingForSurface: " + + mWaitingForSurface + + " mWidth: " + + mWidth + + " mHeight: " + + mHeight + + " mRequestRender: " + + mRequestRender + + " mRenderMode: " + + mRenderMode + + " mRequestedSwapMode: " + + mRequestedSwapMode); + } + mGLThreadManager.wait(); + } + } // end of synchronized(mGLThreadManager) + + if (event != null) { + event.run(); + event = null; + continue; + } + + if (createEglSurface) { + if (LOG_SURFACE) { + Log.w(LOG_TAG, "egl createSurface"); + } + if (mEglHelper.createSurface()) { + synchronized (mGLThreadManager) { + mFinishedCreatingEglSurface = true; + mGLThreadManager.notifyAll(); + } + } else { + synchronized (mGLThreadManager) { + mFinishedCreatingEglSurface = true; + mSurfaceIsBad = true; + mGLThreadManager.notifyAll(); + } + continue; + } + createEglSurface = false; + // If we made a new surface, the swap mode defaults to queued. + swapMode = SWAPMODE_QUEUED; + } + + if (createGlInterface) { + gl = (GL10) mEglHelper.createGL(); + + createGlInterface = false; + } + + if (createEglContext) { + if (LOG_RENDERER) { + Log.w(LOG_TAG, "onSurfaceCreated"); + } + CardboardGLSurfaceView view = mCardboardGLSurfaceViewWeakRef.get(); + if (view != null) { + view.mRenderer.onSurfaceCreated(gl, mEglHelper.mEglConfig); + } + createEglContext = false; + } + + if (sizeChanged) { + if (LOG_RENDERER) { + Log.w(LOG_TAG, "onSurfaceChanged(" + w + ", " + h + ")"); + } + CardboardGLSurfaceView view = mCardboardGLSurfaceViewWeakRef.get(); + if (view != null) { + view.mRenderer.onSurfaceChanged(gl, w, h); + } + sizeChanged = false; + } + + if (applySwapMode) { + mEglHelper.setEglSurfaceAttrib( + EGL14.EGL_RENDER_BUFFER, + swapMode == SWAPMODE_SINGLE ? EGL14.EGL_SINGLE_BUFFER : EGL14.EGL_BACK_BUFFER); + mEglHelper.setEglSurfaceAttrib( + EglHelper.EGL_FRONT_BUFFER_AUTO_REFRESH, + swapMode == SWAPMODE_SINGLE ? EGL14.EGL_TRUE : EGL14.EGL_FALSE); + } + + if (LOG_RENDERER_DRAW_FRAME) { + Log.w(LOG_TAG, "onDrawFrame tid=" + getId()); + } + { + CardboardGLSurfaceView view = mCardboardGLSurfaceViewWeakRef.get(); + if (view != null) { + view.mRenderer.onDrawFrame(gl); + } + } + + if (applySwapMode || swapMode == SWAPMODE_QUEUED) { + int swapError = mEglHelper.swap(); + switch (swapError) { + case EGL10.EGL_SUCCESS: + break; + case EGL11.EGL_CONTEXT_LOST: + if (LOG_SURFACE) { + Log.i(LOG_TAG, "egl context lost tid=" + getId()); + } + lostEglContext = true; + break; + default: + // Other errors typically mean that the current surface is bad, + // probably because the SurfaceView surface has been destroyed, + // but we haven't been notified yet. + // Log the error to help developers understand why rendering stopped. + EglHelper.logEglErrorAsWarning(LOG_TAG, "eglSwapBuffers", swapError); + + // b/29648121: Swap will return egl errors even when everything is working fine. + // Because we won't call swap more than a few times when in the alternate swap + // modes, we ignore the error instead of killing the GL thread. + if (swapMode == SWAPMODE_QUEUED) { + synchronized (mGLThreadManager) { + mSurfaceIsBad = true; + mGLThreadManager.notifyAll(); + } + } + break; + } + } + + if (wantRenderNotification) { + doRenderNotification = true; + wantRenderNotification = false; + } + } + + } finally { + /* + * clean-up everything... + */ + synchronized (mGLThreadManager) { + stopEglSurfaceLocked(); + stopEglContextLocked(); + } + } + } + + public boolean ableToDraw() { + return mHaveEglContext && mHaveEglSurface && readyToDraw(); + } + + private boolean readyToDraw() { + // Keep spinning until all of the following have become true. + return (!mPaused) + && mHasSurface + && (!mSurfaceIsBad) + && (mWidth > 0) + && (mHeight > 0) + && (mRequestRender || (mRenderMode == RENDERMODE_CONTINUOUSLY)); + } + + public void setRenderMode(int renderMode) { + if (!((RENDERMODE_WHEN_DIRTY <= renderMode) && (renderMode <= RENDERMODE_CONTINUOUSLY))) { + throw new IllegalArgumentException("renderMode"); + } + synchronized (mGLThreadManager) { + mRenderMode = renderMode; + mGLThreadManager.notifyAll(); + } + } + + public void setSwapMode(int swapMode) { + if (!((SWAPMODE_QUEUED <= swapMode) && (swapMode <= SWAPMODE_MANUAL))) { + throw new IllegalArgumentException("swapMode"); + } + synchronized (mGLThreadManager) { + mRequestedSwapMode = swapMode; + mGLThreadManager.notifyAll(); + } + } + + public int getRenderMode() { + synchronized (mGLThreadManager) { + return mRenderMode; + } + } + + public int getSwapMode() { + synchronized (mGLThreadManager) { + return mRequestedSwapMode; + } + } + + public void requestRender() { + synchronized (mGLThreadManager) { + mRequestRender = true; + mGLThreadManager.notifyAll(); + } + } + + public void requestRenderAndWait() { + synchronized (mGLThreadManager) { + // If we are already on the GL thread, this means a client callback + // has caused reentrancy, for example via updating the SurfaceView parameters. + // We will return to the client rendering code, so here we don't need to + // do anything. + if (Thread.currentThread() == this) { + return; + } + + mWantRenderNotification = true; + mRequestRender = true; + mRenderComplete = false; + + mGLThreadManager.notifyAll(); + + while (!mExited && !mPaused && !mRenderComplete && ableToDraw()) { + try { + mGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void surfaceCreated() { + synchronized (mGLThreadManager) { + if (LOG_THREADS) { + Log.i(LOG_TAG, "surfaceCreated tid=" + getId()); + } + mHasSurface = true; + mFinishedCreatingEglSurface = false; + mGLThreadManager.notifyAll(); + while (mWaitingForSurface && !mFinishedCreatingEglSurface && !mExited) { + try { + mGLThreadManager.wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void surfaceDestroyed(Runnable r) { + synchronized (mGLThreadManager) { + if (LOG_THREADS) { + Log.i(LOG_TAG, "surfaceDestroyed tid=" + getId()); + } + mHasSurface = false; + if (r != null) { + mEventQueue.add(r); + } + mGLThreadManager.notifyAll(); + while ((!mWaitingForSurface) && (!mExited)) { + try { + mGLThreadManager.wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void onPause(Runnable r) { + synchronized (mGLThreadManager) { + if (LOG_PAUSE_RESUME) { + Log.i(LOG_TAG, "onPause tid=" + getId()); + } + mRequestPaused = true; + if (r != null) { + mEventQueue.add(r); + } + mGLThreadManager.notifyAll(); + while ((!mExited) && (!mPaused)) { + if (LOG_PAUSE_RESUME) { + Log.i(LOG_MAIN_THREAD_TAG, "onPause waiting for mPaused."); + } + try { + mGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void onResume() { + synchronized (mGLThreadManager) { + if (LOG_PAUSE_RESUME) { + Log.i(LOG_TAG, "onResume tid=" + getId()); + } + mRequestPaused = false; + mRequestRender = true; + mRenderComplete = false; + mGLThreadManager.notifyAll(); + while ((!mExited) && mPaused && (!mRenderComplete)) { + if (LOG_PAUSE_RESUME) { + Log.i(LOG_MAIN_THREAD_TAG, "onResume waiting for !mPaused."); + } + try { + mGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void onWindowResize(int w, int h) { + synchronized (mGLThreadManager) { + mWidth = w; + mHeight = h; + mSizeChanged = true; + mRequestRender = true; + mRenderComplete = false; + + // If we are already on the GL thread, this means a client callback + // has caused reentrancy, for example via updating the SurfaceView parameters. + // We need to process the size change eventually though and update our EGLSurface. + // So we set the parameters and return so they can be processed on our + // next iteration. + if (Thread.currentThread() == this) { + return; + } + + mGLThreadManager.notifyAll(); + + // Wait for thread to react to resize and render a frame + while (!mExited && !mPaused && !mRenderComplete && ableToDraw()) { + if (LOG_SURFACE) { + Log.i( + LOG_MAIN_THREAD_TAG, + "onWindowResize waiting for render complete from tid=" + getId()); + } + try { + mGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void requestExitAndWait() { + // don't call this from GLThread thread or it is a guaranteed + // deadlock! + synchronized (mGLThreadManager) { + mShouldExit = true; + mGLThreadManager.notifyAll(); + while (!mExited) { + try { + mGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void requestReleaseEglContextLocked() { + mShouldReleaseEglContext = true; + mGLThreadManager.notifyAll(); + } + + /** + * Queue an "event" to be run on the GL rendering thread. + * @param r the runnable to be run on the GL rendering thread. + */ + public void queueEvent(Runnable r) { + if (r == null) { + throw new IllegalArgumentException("r must not be null"); + } + synchronized (mGLThreadManager) { + mEventQueue.add(r); + mGLThreadManager.notifyAll(); + } + } + + // Once the thread is started, all accesses to the following member + // variables are protected by the mGLThreadManager monitor + private boolean mShouldExit; + private boolean mExited; + private boolean mRequestPaused; + private boolean mPaused; + private boolean mHasSurface; + private boolean mSurfaceIsBad; + private boolean mWaitingForSurface; + private boolean mHaveEglContext; + private boolean mHaveEglSurface; + private boolean mFinishedCreatingEglSurface; + private boolean mShouldReleaseEglContext; + private int mWidth; + private int mHeight; + private int mRenderMode; + private int mRequestedSwapMode; + private boolean mRequestRender; + private boolean mWantRenderNotification; + private boolean mRenderComplete; + private ArrayList mEventQueue = new ArrayList(); + private boolean mSizeChanged = true; + + // End of member variables protected by the mGLThreadManager monitor. + + private EglHelper mEglHelper; + + /** + * Set once at thread construction time, nulled out when the parent view is garbage + * called. This weak reference allows the CardboardGLSurfaceView to be garbage collected while + * the GLThread is still alive. + */ + private WeakReference mCardboardGLSurfaceViewWeakRef; + + private static class GLThreadManager { + private static final String TAG = "GLThreadManager"; + + public synchronized void threadExiting(GLThread thread) { + if (LOG_THREADS) { + Log.i(LOG_TAG, "exiting tid=" + thread.getId()); + } + thread.mExited = true; + notifyAll(); + } + + /* + * Releases the EGL context. Requires that we are already in the + * mGLThreadManager monitor when this is called. + */ + public void releaseEglContextLocked(GLThread thread) { + notifyAll(); + } + } + + private final GLThreadManager mGLThreadManager = new GLThreadManager(); + + } + + static class LogWriter extends Writer { + + @Override + public void close() { + flushBuilder(); + } + + @Override + public void flush() { + flushBuilder(); + } + + @Override + public void write(char[] buf, int offset, int count) { + for (int i = 0; i < count; i++) { + char c = buf[offset + i]; + if (c == '\n') { + flushBuilder(); + } else { + mBuilder.append(c); + } + } + } + + private void flushBuilder() { + if (mBuilder.length() > 0) { + Log.v(TAG, mBuilder.toString()); + mBuilder.delete(0, mBuilder.length()); + } + } + + private StringBuilder mBuilder = new StringBuilder(); + } + + private void checkRenderThreadState() { + if (mGLThread != null) { + throw new IllegalStateException("setRenderer has already been called for this instance."); + } + } + + private final WeakReference mThisWeakRef = + new WeakReference(this); + private GLThread mGLThread; + private Renderer mRenderer; + private boolean mDetached; + private EGLConfigChooser mEGLConfigChooser; + private EGLContextFactory mEGLContextFactory; + private EGLWindowSurfaceFactory mEGLWindowSurfaceFactory; + private GLWrapper mGLWrapper; + private int mDebugFlags; + private int mEGLContextClientVersion; + private boolean mPreserveEGLContextOnPause; + private boolean mPreserveGlThreadOnDetachedFromWindow; +} diff --git a/java_api/src/main/java/com/google/cardboard/sdk/CardboardLayout.java b/java_api/src/main/java/com/google/cardboard/sdk/CardboardLayout.java new file mode 100644 index 00000000..26c8654f --- /dev/null +++ b/java_api/src/main/java/com/google/cardboard/sdk/CardboardLayout.java @@ -0,0 +1,138 @@ +/* + * Copyright 2020 Google LLC + * + * 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.cardboard.sdk; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ImageButton; + +/** + * This class holds a view which results from inflating {@code R.layout.ui_layer}. Provides an easy + * way to set and control the Cardboard layout. This class also includes some methods to set the + * visibility of the inner views (back and settings buttons, and alignment marker), making it + * suitable for both monocular and stereo layouts. Setters for the buttons' callbacks are also + * provided within this class. + */ +public class CardboardLayout { + private final Handler handler; + private final FrameLayout alignmentMarker; + private final ImageButton backButton; + private final ImageButton settingsButton; + private final View view; + + /** + * Creates a new Cardboard Layout object. + * + *

Inflates and adds the {@code R.layout.ui_layer}. It also loads all the inner views. + * + * @param[in] context Used to build a LayoutInflater and inflate the layout. + * @param[in] parent A ViewGroup to get the LayoutParams from. It won't be attached to it. + */ + public CardboardLayout(Context context, ViewGroup parent) { + handler = new Handler(Looper.getMainLooper()); + + LayoutInflater inflater = + (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + view = inflater.inflate(R.layout.cardboard_ui, parent, false); + + alignmentMarker = (FrameLayout) view.findViewById(R.id.ui_alignment_marker); + backButton = (ImageButton) view.findViewById(R.id.ui_back_button); + settingsButton = (ImageButton) view.findViewById(R.id.ui_settings_button); + } + + public View getView() { + return view; + } + + /** Shows all widgets from the layout. */ + public void showAll() { + showAlignmentMarker(); + showBackButton(); + showSettingsButton(); + } + + /** Hides all widgets from the layout. */ + public void hideAll() { + hideAlignmentMarker(); + hideBackButton(); + hideSettingsButton(); + } + + /** Shows alignment marker widget. */ + public void showAlignmentMarker() { + handler.post( + () -> { + alignmentMarker.setVisibility(View.VISIBLE); + alignmentMarker.bringToFront(); + }); + } + + /** Hides alignment marker widget. */ + public void hideAlignmentMarker() { + handler.post(() -> alignmentMarker.setVisibility(View.INVISIBLE)); + } + + /** Shows back button (X) widget. */ + public void showBackButton() { + handler.post( + () -> { + backButton.setVisibility(View.VISIBLE); + backButton.bringToFront(); + }); + } + + /** Hides back button (X) widget. */ + public void hideBackButton() { + handler.post(() -> backButton.setVisibility(View.INVISIBLE)); + } + + /** Shows settings button (gear) widget. */ + public void showSettingsButton() { + handler.post( + () -> { + settingsButton.setVisibility(View.VISIBLE); + settingsButton.bringToFront(); + }); + } + + /** Hides settings button (gear) widget. */ + public void hideSettingsButton() { + handler.post(() -> settingsButton.setVisibility(View.INVISIBLE)); + } + + /** + * Sets a {@code Runnable} as the back button event handler. + * + * @param[in] listener A {@code Runnable} to be executed when the back button is clicked. + */ + public void setOnBackButtonClick(Runnable listener) { + backButton.setOnClickListener((View v) -> listener.run()); + } + + /** + * Sets a {@code Runnable} as the settings button event handler. + * + * @param[in] listener A {@code Runnable} to be executed when the settings button is clicked. + */ + public void setOnSettingskButtonClick(Runnable listener) { + settingsButton.setOnClickListener((View v) -> listener.run()); + } +} diff --git a/java_api/src/main/java/com/google/cardboard/sdk/CardboardView.java b/java_api/src/main/java/com/google/cardboard/sdk/CardboardView.java new file mode 100644 index 00000000..f50bed9c --- /dev/null +++ b/java_api/src/main/java/com/google/cardboard/sdk/CardboardView.java @@ -0,0 +1,1201 @@ +/* + * Copyright 2019 Google LLC + * + * 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.cardboard.sdk; + +import static android.opengl.GLU.gluErrorString; + +import android.content.Context; +import android.opengl.GLES20; +import android.opengl.GLSurfaceView; +import android.opengl.GLSurfaceView.EGLWindowSurfaceFactory; +import android.opengl.Matrix; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.SystemClock; +import android.util.AttributeSet; +import android.util.Log; +import android.view.Choreographer; +import android.view.MotionEvent; +import android.view.SurfaceView; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import com.google.cardboard.sdk.nativetypes.EyeTextureDescription; +import com.google.cardboard.sdk.nativetypes.EyeType; +import com.google.cardboard.sdk.nativetypes.Mesh; +import com.google.cardboard.sdk.utils.MathUtils; +import java.nio.IntBuffer; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +/** + * Wrapper around {@code CardboardViewApi} and {@code GLSurfaceView}. Cardboard SDK provides stereo + * rendering only but this class offers stereo and monocular rendering seamlessly by reusing the + * same {@code GLSurfaceView}. + */ +public class CardboardView extends FrameLayout { + /** Defines an interface of an Eye, a useful bundle to the renderer when drawing. */ + public static class Eye { + public static final int LEFT = EyeType.LEFT; + public static final int RIGHT = EyeType.RIGHT; + public static final int MONOCULAR = 2; + + /** Default vertical FOV for monocular mode. */ + static final float MONOCULAR_FOV_Y_DEGREES = 45.0f; + + /** + * Holds the eye type, one value in the set {{@code LEFT}, {@code RIGHT}, {@code MONOCULAR}}. + */ + private final int eyeType; + + /** Holds a reference to {@code CardboardViewApi} to compute eye matrices. */ + private final CardboardViewApi cardboardViewApi; + + /** Holds the Transformation of the from the head after the head was rotated */ + private final float[] eyeViewMatrix; + + /** Holds {@code LensDistortion} eye from head matrix for a device */ + private final float[] eyeFromHeadMatrix = new float[16]; + + /** Holds {@code LensDistortion} eye projection matrix for a device */ + private final float[] projectionMatrix = new float[16]; + + public Eye(int eyeType, CardboardViewApi cardboardViewApi) { + this.eyeType = eyeType; + eyeViewMatrix = new float[16]; + this.cardboardViewApi = cardboardViewApi; + resetEyeFromHeadMatrix(); + } + + public void resetEyeFromHeadMatrix() { + if (this.cardboardViewApi == null) { + return; + } + + if (this.eyeType == MONOCULAR) { + this.cardboardViewApi.getEyeFromHeadMatrix(EyeType.LEFT, eyeFromHeadMatrix); + } else { + this.cardboardViewApi.getEyeFromHeadMatrix(this.eyeType, eyeFromHeadMatrix); + } + } + + /** + * Gets the eye type. + * + * @return An integer describing the which eye this class represents. It can be one of {{@code + * LEFT}, {@code RIGHT}, {@code MONOCULAR}}. + */ + public int getEyeType() { + return eyeType; + } + + /** + * Gets the eye from head matrix. + * + * @return A float 4x4 matrix representing the eye from head matrix. + */ + public float[] getEyeFromHead() { + return eyeFromHeadMatrix; + } + + public void applyHeadView(float[] headView) { +// resetEyeFromHeadMatrix(); + Matrix.multiplyMM(eyeViewMatrix, 0, eyeFromHeadMatrix, 0, headView, 0); + } + + /** + * Returns a matrix that transforms from the camera to the current eye. This transform is the + * multiplication of the head rotation and the eye from head transformation. + * + * @return A 4x4 column-major representing the transformation from the camera to this eye. + */ + public float[] getEyeView() { + return eyeViewMatrix; + } + + /** + * Gets the perspective matrix for the input z-axis clipping planes. + * + * @param[in] zNear Near clip plane z-axis coordinate. + * @param[in] zFar Far clip plane z-axis coordinate. + * @return A float 4x4 matrix representing the perspective matrix. + */ + public float[] getPerspective(float zNear, float zFar) { + if (eyeType == MONOCULAR) { + computeMonocularPerspectiveMatrix(zNear, zFar); + } else { + cardboardViewApi.getEyeProjectionMatrix(eyeType, zNear, zFar, projectionMatrix); + } + return projectionMatrix; + } + + /** + * Constructs a float array with device's field of view angles. + * + * @return A float array whose elements are "[left, right, bottom, top]" angles. + */ + public float[] getFieldOfView() { + final float[] fov = new float[4]; + fov[0] = cardboardViewApi.getFieldOfViewParamsLeft(); + fov[1] = cardboardViewApi.getFieldOfViewParamsRight(); + fov[2] = cardboardViewApi.getFieldOfViewParamsBottom(); + fov[3] = cardboardViewApi.getFieldOfViewParamsTop(); + return fov; + } + + /** + * Computes in {@code projectionMatrix} the projection matrix for monocular rendering mode. + * + *

Screen size is extracted from {@code cardboardViewApi} and from it, the viewport aspect + * ratio is derived. A fixed vertical angular span is used in conjunction with the z-axis + * clipping planes. + * + * @param zNear Near clip plane z-axis coordinate. + * @param zFar Far clip plane z-axis coordinate. + */ + private void computeMonocularPerspectiveMatrix(float zNear, float zFar) { + // For monocular rendering always use a fixed vertical FOV and calculate the horizontal FOV + // correspondingly using the aspect ratio of the screen. + float aspectRatio = + cardboardViewApi.getScreenParamsWidth() + / (float) cardboardViewApi.getScreenParamsHeight(); + Matrix.perspectiveM(projectionMatrix, 0, MONOCULAR_FOV_Y_DEGREES, aspectRatio, zNear, zFar); + } + } + + /** + * Defines a rendering interface to be consumed by the {@code GLSurfaceView} when set. + * + *

Note: all callbacks will be executed in the rendering thread. + */ + public interface Renderer { + /** + * Called when a new frame is about to be drawn. + * + *

Allows to differenciate between rendering eye views and different frames. Any per-frame + * operations not specific to a single view should happen here. + * + * @param headTransform The head transformation in the new frame. It is a 4x4 matrix. + */ + public void onNewFrame(HeadTransform headTransform); + + /** + * Requests to draw the contents from the point of view of an eye. + * + *

When monocular rendering mode is set, {@code onDrawEye()} will be called once per frame + * with monocular eye information. When stereo rendering mode is set, this method is called for + * the left eye first and then for the right eye. + * + * @param eye The eye type and matrix information. + */ + public void onDrawEye(Eye eye); + + /** Called before a frame is finished. */ + public void onFinishFrame(Viewport viewport); + + /** + * Called when there is a change in the surface dimensions. + * + * @param width New width of the surface for a single eye in pixels. + * @param height New height of the surface for a single eye in pixels. + */ + public void onSurfaceChanged(int width, int height); + + /** + * Called when the surface is created or recreated. + * + * @param config The EGL configuration used when creating the surface. + */ + public void onSurfaceCreated(EGLConfig config); + + /** + * Called on the renderer thread when the thread is shutting down. + * + *

Allows releasing GL resources and performing shutdown operations in the renderer thread. + * Called only if there was a previous call to {@link #onSurfaceCreated(EGLConfig) + * onSurfaceCreated}. + */ + public void onRendererShutdown(); + } + + /** Aggregate type to hold eye specific stereo eye data. */ + private static class EyeData { + /** Eye configuration. */ + public final Eye eye; + + /** Cardboard SDK eye textrure description. */ + public final EyeTextureDescription textureDescription; + + public EyeData(int eyeType, CardboardViewApi cardboardViewApi) { + eye = new Eye(eyeType, cardboardViewApi); + textureDescription = new EyeTextureDescription(); + } + } + + /** + * Interface of a safe surface view. + * + *

This interface is two fold: provides by means of the adapter pattern a common interface to + * interact with {@code CardboardGLSurfaceView} and {@code GLSurfaceView}, and subclasses them to + * call a shutdown runnable when detached from the window. + */ + private static interface SafeSurfaceView { + /** + * The following list of methods are almost all that {@code GLView} offers. {@code + * GLSurfaceView} implements such interface, but {@code CardboardGLSurfaceView} doesn't. + */ + public void setEGLContextClientVersion(int version); + + public void setRenderer(android.opengl.GLSurfaceView.Renderer renderer); + + public void setRenderMode(int mode); + + public void onPause(); + + public void onResume(); + + public void setPreserveEGLContextOnPause(boolean preserveOnPause); + + public void setOnTouchListener(android.view.View.OnTouchListener listener); + + public void setLayoutParams(ViewGroup.LayoutParams glViewLayoutParams); + /** The following list of methods are part of the {@code GLSurfaceView} public API. */ + public void setEGLConfigChooser( + int redSize, int greenSize, int blueSize, int alphaSize, int depthSize, int stencilSize); + + public void setEGLWindowSurfaceFactory(EGLWindowSurfaceFactory factory); + + public void queueEvent(Runnable r); + /** + * The following two methods conform the extended "safe" interface of the surface to execute a + * runnable when the view is detached. + */ + public void setOnViewDetachedRunnable(Runnable onViewDetachedRunnable); + + public boolean isViewAttached(); + + public void requestRender(); + } + + /** + * Implementation of {@code SafeSurfaceView} for {@code CardboardGLSurfaceView}. + * + *

Most of the methods forwards calls to {@code CardboardGLSurfaceView} implementation. The + * rest offers mechanisms to call a runnable when the view is detached from the window. + */ + private static class SafeCardboardGLSurfaceView extends CardboardGLSurfaceView + implements SafeSurfaceView { + private boolean isViewAttached; + private Runnable onViewDetachedRunnable; + + /** + * Constructs a SafeCardboardGLSurfaceView. + * + * @param[in] context Unused parameter within this class, required by parent class. It is + * generally an Activity instance or wraps one. + */ + public SafeCardboardGLSurfaceView(Context context) { + super(context); + isViewAttached = false; + } + + @Override + protected void onDetachedFromWindow() { + if (onViewDetachedRunnable != null) { + onViewDetachedRunnable.run(); + } + isViewAttached = false; + super.onDetachedFromWindow(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + isViewAttached = true; + } + + @Override + public void setOnViewDetachedRunnable(Runnable onViewDetachedRunnable) { + this.onViewDetachedRunnable = onViewDetachedRunnable; + } + + @Override + public boolean isViewAttached() { + return isViewAttached; + } + } + + /** + * Implementation of {@code SafeSurfaceView} for {@code GLSurfaceView}. + * + *

Most of the methods forwards calls to {@code GLSurfaceView} implementation. The rest offers + * mechanisms to call a runnable when the view is detached from the window. + */ + private static class SafeGLSurfaceView extends GLSurfaceView implements SafeSurfaceView { + private boolean isViewAttached; + private Runnable onViewDetachedRunnable; + + /** + * Constructs a SafeGLSurfaceView. + * + * @param[in] context Unused parameter within this class, required by parent class. It is + * generally an Activity instance or wraps one. + */ + public SafeGLSurfaceView(Context context) { + super(context); + isViewAttached = false; + } + + @Override + protected void onDetachedFromWindow() { + if (onViewDetachedRunnable != null) { + onViewDetachedRunnable.run(); + } + isViewAttached = false; + super.onDetachedFromWindow(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + isViewAttached = true; + } + + @Override + public void setOnViewDetachedRunnable(Runnable onViewDetachedRunnable) { + this.onViewDetachedRunnable = onViewDetachedRunnable; + } + + @Override + public boolean isViewAttached() { + return isViewAttached; + } + } + + /** Thread with its own Looper to serve Choreographer. */ + private static class ChoreographerThread extends Thread { + private volatile Handler handler; + + @Override + public void run() { + Looper.prepare(); + handler = + new Handler(Looper.myLooper()) { + @Override + public void handleMessage(Message msg) {} + }; + Looper.loop(); + } + + /** + * Posts a message to Choreographer to request the rendering of @p view upon the next VSYNC + * callback. + * + * @param[in] view A SafeSurfaceView to request the rendering. + */ + public synchronized void requestRenderOnVsyncEvent(final SafeSurfaceView view) { + if (handler != null) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + handler.post( + () -> Choreographer.getInstance().postFrameCallback((t) -> view.requestRender())); + } else { + handler.post( + () -> Choreographer.getInstance().postVsyncCallback((t) -> view.requestRender())); + } + } + } + + /** + * When {@code handler} is not null, quits the Looper making it not possible to continue sending + * messages. + */ + public synchronized void stopLooper() { + if (handler != null) { + handler.getLooper().quit(); + handler = null; + } + } + } + + private static final String TAG = CardboardView.class.getSimpleName(); + + /** Timeout in milliseconds to wait for the Choreographer thread to join. */ + private static final int CHOREOGRAPHER_THREAD_JOIN_TIMEOUT_MS = 100; + + /** Tracks when screen parameters changed. */ + private boolean screenParamsChanged; + + /** Tracks when device parameters changed. */ + private boolean deviceParamsChanged; + + /** Holds left eye specific data. */ + private EyeData leftEyeData; + + /** Holds right eye specific data. */ + private EyeData rightEyeData; + + /** Holds monocular eye specific data. */ + private Eye monocularEye; + + /** OpenGL depth render buffer. */ + private final int[] depthRenderBuffer = new int[1]; + + /** OpenGL frame buffer. */ + private final int[] framebuffer = new int[1]; + + /** OpenGL texture. */ + private final int[] texture = new int[1]; + + /** Cardboard SDK API. */ + private CardboardViewApi cardboardViewApi; + + /** OpenGL Surface. */ + private SafeSurfaceView glView; + + /** Custom client renderer. */ + private Renderer renderer; + + /** Stereo rendering mode status. */ + private boolean stereoRenderMode; + + /** Cardboard layout manager. */ + private CardboardLayout cardboardLayout; + + /** + * Alias of {@code stereoRenderMode} updated when the frame is rendered. It avoids any change in + * the rendering mode while the frame is drawn. + */ + private boolean currentStereoRenderMode; + + private long lastLogTime; + + /** Tracks whether a shutdown sequence has started. */ + private volatile boolean shutdownCalled = false; + + /** Tracks whether to use Gvr GlSurfaceView or Android's. */ + private static boolean useCardboardGlSurfaceView = false; + + /** Thread to execute Choreographer VSync callbacks. */ + private ChoreographerThread choreographerThread; + + /** + * Selects which GlSurfaceView to use. + * + *

Consumers of this class should call this class method before constructing an instance. By + * default, the Android GlSurfaceView will be preferred (wrapped with the SafeGlSurfaceView class) + * if this function is not called. + * + * @param[in] useCardboardGlSurfaceView True when Gvr GlSurfaceView must be used, else false. + */ + public static void setUseCardboardGlSurfaceView(boolean useCardboardGlSurfaceView) { + CardboardView.useCardboardGlSurfaceView = useCardboardGlSurfaceView; + Log.w(TAG, "CardboardSV(" + useCardboardGlSurfaceView + ")"); + } + + /** + * Constructs a CardboardView. + * + * @param[in] context The current Context. It is generally an Activity instance or wraps one. The + * following is the list of methods that will be queried: getFilesDir(), getPackageManager(), + * getResources(), getSystemService(Context.WINDOW_SERVICE), startActivity(Intent), + * getContentResolver(), getDisplay(). + */ + public CardboardView(Context context) { + super(context); + Log.w(TAG, "CW(C)"); + initialize(context); + } + + /** + * Constructs a CardboardView. + * + * @param[in] context The current Context. It is generally an Activity instance or wraps one. The + * following is the list of methods that will be queried: getFilesDir(), getPackageManager(), + * getResources(), getSystemService(Context.WINDOW_SERVICE), startActivity(Intent), + * getContentResolver(), getDisplay(). + * @param[in] attrs The custom AttributeSet. + */ + public CardboardView(Context context, AttributeSet attributeSet) { + super(context, attributeSet); + Log.w(TAG, "CW(C,A)"); + initialize(context); + } + + /** + * Initializes a CardboardView. + * + *

Constructs the {@code CardboardViewApi} and the {@code GLSurfaceView}. For the latter, a + * custom {@code GLSurfaceView.Renderer} is created to forward calls to private methods that will + * handle the management of the GL resources and {@code CardboardViewApi}. + * + *

Monocular rendering is configured, meaning that {@code stereoRenderMode} is initialized with + * false. + * + * @param[in] context The current Context. It is generally an Activity instance or wraps one. See + * {@code CardboardView(Context)} for more details. + */ + private void initialize(Context context) { + cardboardViewApi = new CardboardViewApi(context); + + renderer = null; + + // Sets and adds GLSurfaceView to CardboardView layout. + if (useCardboardGlSurfaceView) { + Log.w(TAG, "Using Cardboard Surface"); + glView = new SafeCardboardGLSurfaceView(context); + } else { + Log.w(TAG, "Using Android Surface"); + glView = new SafeGLSurfaceView(context); + } + + glView.setEGLContextClientVersion(2); + // The following line makes the surface to preserve the context between resumed-paused state + // transitions. + // GvrView sets it which makes dependent and already integrated code assume that the Gl + // context will be preserved. + // When it is set to false (or default is used, i.e. false), client code will experiment calls + // to Renderer::onSurfaceCreated() after resuming the GlSurfaceView from a prior pause state. + // The callback is used to request a Gl context creation because the surface has been created + // for the first time or re-created. + // + // See b/186201897 to understand what this line could cause if not set. + // See {@code GlSurfaceView::setPreserveEGLContextOnPause()} for further details. + glView.setPreserveEGLContextOnPause(true); + ViewGroup.LayoutParams glViewLayoutParams = + new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); + glView.setLayoutParams(glViewLayoutParams); + addView((SurfaceView) glView); + + // Sets and adds CardboardLayout to CardboardView layout. + cardboardLayout = new CardboardLayout(context, this); + setStereoRenderMode(false); + screenParamsChanged = false; + deviceParamsChanged = false; + + addView(cardboardLayout.getView()); + } + + /** + * Sets the renderer that will be called from {@code GLSurfaceView}'s callbacks. + * + * @param[in] renderer The CardboardView.Renderer implementation to be consumed by GLSurfaceView. + */ + public void setRenderer(Renderer renderer) { + Log.w(TAG, "SR"); + this.renderer = renderer; + lastLogTime = SystemClock.uptimeMillis(); + glView.setRenderer( + new GLSurfaceView.Renderer() { + @Override + public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) { + CardboardView.this.onSurfaceCreated(eglConfig); + } + + @Override + public void onSurfaceChanged(GL10 gl10, int width, int height) { + CardboardView.this.onSurfaceChanged(width, height); + } + + @Override + public void onDrawFrame(GL10 gl10) { + CardboardView.this.onDrawFrame(); + if (!shutdownCalled && choreographerThread != null) { + choreographerThread.requestRenderOnVsyncEvent(glView); + } + } + }); + glView.setRenderMode(CardboardGLSurfaceView.RENDERMODE_WHEN_DIRTY); + choreographerThread = new ChoreographerThread(); + choreographerThread.start(); + choreographerThread.requestRenderOnVsyncEvent(glView); + } + + /** + * Gets the enabled rendering mode. + * + * @return true When stereo rendering mode is enabled. Otherwise, monocular rendering is enabled. + */ + public boolean isStereoRenderModeEnabled() { + return stereoRenderMode; + } + + /** + * Sets the enabled rendering mode. + * + *

This method can be called from any thread. However, the change (if any) will be effective + * next time the {@code GLSurfaceView} in the rendering thead executes {@code onDrawFrame()} + * callback. + * + * @param[in] enable When true, stereo rendering mode will be enabled. Otherwise, monocular + * rendering becomes effective. + */ + public void setStereoRenderMode(boolean enable) { + Log.w(TAG, "SSRMode(" + enable + ")"); + + stereoRenderMode = enable; + screenParamsChanged = true; + + // Setups the layout according to the selected mode. + if (enable) { + cardboardLayout.showAll(); + } else { + cardboardLayout.hideAll(); + } + } + + /** + * Pauses the {@code HeadTracker} module and the {@code GLSurfaceView}. + * + *

Generally, this method should be called from the Activity that contains it at {@code + * onPause()} callback. + * + *

Preconditions: Must not be called before a renderer has been set as it forwards the call to + * the {@code GLSurfaceView}. + */ + public void onPause() { + Log.w(TAG, "OP"); + cardboardViewApi.pauseHeadTracker(); + glView.onPause(); + } + + /** + * Resumes the {@code HeadTracker} module and the {@code GLSurfaceView}. + * + *

Generally, this method should be called from the Activity that contains it at {@code + * onResume()} callback. + * + *

Preconditions: Must not be called before a renderer has been set as it forwards the call to + * the {@code GLSurfaceView}. + */ + public void onResume() { + Log.w(TAG, "OR()"); + // Parameters may have changed. + deviceParamsChanged = true; + glView.onResume(); + cardboardViewApi.resumeHeadTracker(); + } + + /** + * Loads device parameter configuration. See {@code CardboardViewApi::loadDeviceParams()} for an + * in detail explanation of how and where parameters are obtained. + */ + public void loadDeviceParams() { + Log.w(TAG, "LDP"); + // Parameters may have changed. + deviceParamsChanged = true; + cardboardViewApi.loadDeviceParams(); + } + + /** Settings (gear) button callback. */ + public void onSettingsButtonClick() { + Log.w(TAG, "OSBC"); + cardboardViewApi.scanViewerQrCode(); + } + + /** Scans viewer parameters. */ + public void scanViewerQrCode() { + Log.w(TAG, "SVQRC"); + cardboardViewApi.scanViewerQrCode(); + } + + /** + * Closes all taken resources by {@code CardboardViewApi}. + * + *

Generally, this method should be called from the Activity that contains it at {@code + * onDestroy()} callback. + */ + public void onDestroy() { + Log.w(TAG, "OD"); + shutdownCalled(); + if (choreographerThread != null) { + try { + choreographerThread.stopLooper(); + choreographerThread.join(CHOREOGRAPHER_THREAD_JOIN_TIMEOUT_MS); + } catch (SecurityException e) { + Log.e(TAG, "OD-SE"); + } catch (InterruptedException e) { + Log.e(TAG, "OD-IE"); + } finally { + choreographerThread = null; + } + } + cardboardViewApi.close(); + } + + /** Forwards the call to the underlying GL view. */ + public void setEGLConfigChooser( + int redSize, int greenSize, int blueSize, int alphaSize, int depthSize, int stencilSize) { + glView.setEGLConfigChooser(redSize, greenSize, blueSize, alphaSize, depthSize, stencilSize); + } + + /** Forwards the call to the underlying GL view. */ + public void setEGLWindowSurfaceFactory(EGLWindowSurfaceFactory factory) { + glView.setEGLWindowSurfaceFactory(factory); + } + + /** Returns whether the GL view is attached. */ + public boolean isGlViewAttached() { + return glView.isViewAttached(); + } + + /** Sets a runnable that should be executed when the GL view is detached. */ + public void setOnViewDetachedRunnable(Runnable onViewDetachedRunnable) { + Log.w(TAG, "SOVDR"); + glView.setOnViewDetachedRunnable(onViewDetachedRunnable); + } + + /** + * Sets a {@code Runnable} to be executed in {@code GLSurfaceView}'s GL Thread. + * + *

Preconditions: Must not be called before a renderer has been set as it forwards the call to + * the {@code GLSurfaceView}. + * + * @param[in] runnable Runnable to execute. + */ + public void queueEvent(Runnable runnable) { + glView.queueEvent(runnable); + } + + /** + * Sets a {@code Runnable} as the back button event handler. + * + * @param[in] listener A {@code Runnable} to be executed when the back button is clicked. + */ + public void setOnBackButtonClick(Runnable listener) { + Log.w(TAG, "SOBBC"); + cardboardLayout.setOnBackButtonClick(listener); + } + + /** + * Sets a {@code Runnable} as the settings button event. + * + * @param[in] listener A {@code Runnable} to be executed when the settings button is clicked. + */ + public void setOnSettingsButtonClick(Runnable listener) { + Log.w(TAG, "SOSBC"); + cardboardLayout.setOnSettingskButtonClick(listener); + } + + /** + * Sets a {@code Runnable} to be executed when the viewer's trigger is pressed (screen is + * touched). + * + * @param[in] runnable Runnable to execute. + */ + public void setOnTriggerEvent(Runnable runnable) { + Log.w(TAG, "SOTE"); + glView.setOnTouchListener( + (v, event) -> { + if (runnable != null && event.getAction() == MotionEvent.ACTION_DOWN) { + runnable.run(); + v.performClick(); + return true; + } + return false; + }); + } + + /** + * Flags from *any* thread that the shutdown sequence has started. + * + *

This method does not replace {@code CardboardView::onDestroy()} which must be still called + * to properly close resources. However, application code can use this flag to prevent {@code + * onSurfaceCreated()}, {@code onSurfaceChanged()} and {@code onDrawFrame()} callbacks from + * running next time the GlSurfaceView executes them before the enqueued run ({@code + * CardboardView::onDestroy()} is typically enqueued in the Gl Thread). + */ + public void shutdownCalled() { + shutdownCalled = true; + } + + /** Updates device parameters and GL resources if needed. */ + private boolean updateDeviceParams() { + // Checks if screen or device parameters changed. + if (!screenParamsChanged && !deviceParamsChanged) { + return true; + } + + // Turns down both flags. Note when they change: + // - screenParamsChanged will be set when the surface changes ({@code onSurfaceChange()}) or + // when the stereoscopic view changes ({@code setStereoRenderMode()}). + // - deviceParamsChanged will be set when the application resumes after a pause state + // ({@code onResume()}) under the assumption that parameters may have changed by other + // application (Android R or below) and when a request to {@code loadDeviceParams()} is + // performed. + screenParamsChanged = false; + deviceParamsChanged = false; + + // Gets saved device parameters. + // Note: the following call does not touch {@code deviceParamsChange}. + cardboardViewApi.loadDeviceParams(); + + // When using monocular rendering, we must ensure to teardown previously configured GL resources + // for stereo rendering. Monocular rendering setup is trivial, so no configuration is needed. + if (!currentStereoRenderMode) { + glTeardown(); + // Recreates the eye data for monocular rendering. + monocularEye = new Eye(Eye.MONOCULAR, cardboardViewApi); + return true; + } + + // Gets distortion meshes. + Mesh leftMesh = cardboardViewApi.getDistortionMesh(EyeType.LEFT); + Mesh rightMesh = cardboardViewApi.getDistortionMesh(EyeType.RIGHT); + + // Recreates the eye data. + leftEyeData = new EyeData(Eye.LEFT, cardboardViewApi); + rightEyeData = new EyeData(Eye.RIGHT, cardboardViewApi); + + glSetup(); + + cardboardViewApi.initializeRenderThread(); + + // Sets distortion meshes. + cardboardViewApi.setMesh(leftMesh, EyeType.LEFT); + cardboardViewApi.setMesh(rightMesh, EyeType.RIGHT); + + checkGlError("updateDeviceParams"); + + return true; + } + + /** + * Setups GLES20 components. + * + *

Modifies the OpenGL global state. In particular: - glGet(GL_TEXTURE_BINDING_2D) - + * glGet(GL_FRAMEBUFFER_BINDING) + */ + private void glSetup() { + if (framebuffer[0] != 0) { + glTeardown(); + } + + // Creates render texture. + GLES20.glGenTextures(1, IntBuffer.wrap(texture)); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture[0]); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexImage2D( + GLES20.GL_TEXTURE_2D, + 0, + GLES20.GL_RGB, + cardboardViewApi.getScreenParamsWidth(), + cardboardViewApi.getScreenParamsHeight(), + 0, + GLES20.GL_RGB, + GLES20.GL_UNSIGNED_BYTE, + null); + + // Sets eyes texture description. + leftEyeData.textureDescription.texture = (long) texture[0]; + leftEyeData.textureDescription.leftU = 0f; + leftEyeData.textureDescription.rightU = 0.5f; + leftEyeData.textureDescription.topV = 1f; + leftEyeData.textureDescription.bottomV = 0f; + + rightEyeData.textureDescription.texture = (long) texture[0]; + rightEyeData.textureDescription.leftU = 0.5f; + rightEyeData.textureDescription.rightU = 1f; + rightEyeData.textureDescription.topV = 1f; + rightEyeData.textureDescription.bottomV = 0f; + + // Generate depth buffer to perform depth test. + GLES20.glGenRenderbuffers(1, IntBuffer.wrap(depthRenderBuffer)); + GLES20.glBindRenderbuffer(GLES20.GL_RENDERBUFFER, depthRenderBuffer[0]); + GLES20.glRenderbufferStorage( + GLES20.GL_RENDERBUFFER, + GLES20.GL_DEPTH_COMPONENT16, + cardboardViewApi.getScreenParamsWidth(), + cardboardViewApi.getScreenParamsHeight()); + checkGlError("Create Render buffer"); + + // Creates the render target. + GLES20.glGenFramebuffers(1, IntBuffer.wrap(framebuffer)); + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, framebuffer[0]); + GLES20.glFramebufferTexture2D( + GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, texture[0], 0); + GLES20.glFramebufferRenderbuffer( + GLES20.GL_FRAMEBUFFER, + GLES20.GL_DEPTH_ATTACHMENT, + GLES20.GL_RENDERBUFFER, + depthRenderBuffer[0]); + + checkGlError("glSetup"); + } + + /** Teardowns GLES20 components. */ + private void glTeardown() { + if (framebuffer[0] == 0) { + return; + } + + GLES20.glDeleteRenderbuffers(1, IntBuffer.wrap(depthRenderBuffer)); + depthRenderBuffer[0] = 0; + GLES20.glDeleteFramebuffers(1, IntBuffer.wrap(framebuffer)); + framebuffer[0] = 0; + GLES20.glDeleteTextures(1, IntBuffer.wrap(texture)); + texture[0] = 0; + + checkGlError("glTeardown"); + } + + /** + * Gets the pose matrix. + * + *

If the head tracker query results in a no-op because it is not initialized, this function + * will return an identity transform. + * + * @return 4x4 pose matrix as a float array. + */ + private float[] getPose() { + float[] posePosition = {0.0f, 0.0f, 0.0f}; + float[] poseOrientation = {0.0f, 0.0f, 0.0f, 1.0f}; + cardboardViewApi.getPose(posePosition, poseOrientation); + return MathUtils.getMatrixFromPose(posePosition, poseOrientation); + } + + /** Inner callback that just redirects to {@code renderer} the call. */ + private void onSurfaceCreated(EGLConfig eglConfig) { + if (shutdownCalled) { + Log.w(TAG, "OSCRS"); + return; + } + if (renderer != null) { + renderer.onSurfaceCreated(eglConfig); + } + } + + /** + * Inner callback that updates {@code cardboardViewApi} screen size and forwards the call to the + * {@code renderer}. + * + * @param width New width of the surface in pixels. + * @param height New height of the surface in pixels. + */ + private void onSurfaceChanged(int width, int height) { + Log.w(TAG, "OSC(" + width + ", " + height + ")"); + if (shutdownCalled) { + Log.w(TAG, "OSCS"); + return; + } + cardboardViewApi.setScreenParams(width, height); + screenParamsChanged = true; + + if (renderer != null) { + try { + renderer.onSurfaceChanged(width, height); + } catch (Throwable throwable) { + errorLogCurrentState(); + throw throwable; + } + } + } + + /** Logs every at roughly 1Hz that the renderer is active and the selected renderer mode. */ + private void logOnDrawFrame() { + final long currentTime = SystemClock.uptimeMillis(); + if ((currentTime - lastLogTime) < 1000) { + return; + } + lastLogTime = currentTime; + Log.w(TAG, "DF"); + } + + /** + * Inner callback that manages the draw frame event. + * + *

Saves the value of {@code stereoRenderMode} in {@code currentStereoRenderMode} and uses the + * latter during the span of this method and preserve the rendering mode while this method is + * executed. Note that it can be changed from the GUI thread while this one runs on the rendering + * one. + * + *

Updates device parameters if necessary and retrieves the head pose to call {@code + * Renderer.onNewFrame()}. Then, the frame buffer is cleared, viewports are set and the client + * rendering via {@code Render.onDrawEye()} happens. Finally, {@code Renderer.onFinishFrame()} is + * called to free any GL resources. + * + *

When {@code currentStereoRenderMode} is true, left and right eyes are rendered using + * Cardboard SDK. Otherwise, calls to Open GL are done to clean the viewport and channels and only + * left eye is rendered. + */ + private void onDrawFrame() { + if (shutdownCalled) { + Log.w(TAG, "DFS"); + return; + } + // Guarantees that stereoRenderMode is preserved while onDrawFrame() is executed. + currentStereoRenderMode = stereoRenderMode; + + logOnDrawFrame(); + + if (!updateDeviceParams()) { + return; + } + + // Updates Head Pose. + if (renderer != null) { + try { + HeadTransform headTransform = new HeadTransform(); + System.arraycopy(getPose(), 0, headTransform.getHeadView(), 0, 16); + + renderer.onNewFrame(headTransform); + } catch (Throwable throwable) { + errorLogCurrentState(); + throw throwable; + } + } + + // Draws the frames. + if (currentStereoRenderMode) { + onRenderStereoFrame(); + } else { + onRenderMonocularFrame(); + } + + // Finishes the frame. + if (renderer != null) { + try { + Viewport viewport = new Viewport(); + viewport.setViewport( + 0, + 0, + cardboardViewApi.getScreenParamsWidth(), + cardboardViewApi.getScreenParamsHeight()); + renderer.onFinishFrame(viewport); + } catch (Throwable throwable) { + errorLogCurrentState(); + throw throwable; + } + } + checkGlError("onDrawFrame"); + } + + /** + * Renders left and right eye using Cardboard SDK. + * + *

When {@code currentStereoRenderMode} is false, it does nothing. + * + *

Modifies the OpenGL global state. In particular: - glGet(GL_FRAMEBUFFER_BINDING) - + * glGet(GL_VIEWPORT) + */ + private void onRenderStereoFrame() { + if (!currentStereoRenderMode) { + return; + } + + // Binds framebuffer. + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, framebuffer[0]); + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT); + + // Draws eyes views. + if (renderer != null) { + GLES20.glViewport( + 0, + 0, + cardboardViewApi.getScreenParamsWidth() / 2, + cardboardViewApi.getScreenParamsHeight()); + try { + renderer.onDrawEye(leftEyeData.eye); + } catch (Throwable throwable) { + errorLogCurrentState(); + throw throwable; + } + + GLES20.glViewport( + cardboardViewApi.getScreenParamsWidth() / 2, + 0, + cardboardViewApi.getScreenParamsWidth() / 2, + cardboardViewApi.getScreenParamsHeight()); + try { + renderer.onDrawEye(rightEyeData.eye); + } catch (Throwable throwable) { + errorLogCurrentState(); + throw throwable; + } + } + + // Renders eye textures to display. + cardboardViewApi.renderEyeToDisplay( + leftEyeData.textureDescription, rightEyeData.textureDescription); + } + + /** + * Renders left eye without any distortion. + * + *

When {@code currentStereoRenderMode} is true, it does nothing. + * + *

Modifies the OpenGL global state. In particular: - glGet(GL_CLEAR_COLOR_VALUE) - + * glGet(GL_VIEWPORT) + */ + private void onRenderMonocularFrame() { + if (currentStereoRenderMode) { + return; + } + + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT); + GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + + if (renderer != null) { + GLES20.glViewport( + 0, 0, cardboardViewApi.getScreenParamsWidth(), cardboardViewApi.getScreenParamsHeight()); + try { + renderer.onDrawEye(monocularEye); + } catch (Throwable throwable) { + errorLogCurrentState(); + throw throwable; + } + } + } + + /** + * Checks GLES20.glGetError and fails quickly if the state isn't GL_NO_ERROR. + * + * @param label Label to report in case of error. + */ + private static void checkGlError(String label) { + int error = GLES20.glGetError(); + int lastError; + if (error != GLES20.GL_NO_ERROR) { + do { + lastError = error; + Log.w(TAG, label + ": glError " + gluErrorString(lastError)); + error = GLES20.glGetError(); + } while (error != GLES20.GL_NO_ERROR); + + throw new RuntimeException("glError " + gluErrorString(lastError)); + } + } + + /** Logs the state of this class instance. */ + private void errorLogCurrentState() { + Log.w( + TAG, + "screenParamsChanged: " + + screenParamsChanged + + " | deviceParamsChanged: " + + deviceParamsChanged + + " | stereoRenderMode: " + + stereoRenderMode + + " | currentStereoRenderMode: " + + currentStereoRenderMode + + " | renderer != null: " + + (renderer != null)); + } +} diff --git a/java_api/src/main/java/com/google/cardboard/sdk/CardboardViewApi.java b/java_api/src/main/java/com/google/cardboard/sdk/CardboardViewApi.java new file mode 100644 index 00000000..b3c01387 --- /dev/null +++ b/java_api/src/main/java/com/google/cardboard/sdk/CardboardViewApi.java @@ -0,0 +1,713 @@ +/* + * Copyright 2019 Google LLC + * + * 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.cardboard.sdk; + +import android.Manifest; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.hardware.display.DisplayManager; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.util.Log; +import android.view.Display; +import android.view.Surface; +import android.view.WindowManager; +import androidx.core.app.ActivityCompat; +import com.google.cardboard.proto.CardboardDevice; +import com.google.cardboard.sdk.deviceparams.CardboardV1DeviceParams; +import com.google.cardboard.sdk.deviceparams.DeviceParamsUtils; +import com.google.cardboard.sdk.nativetypes.EyeTextureDescription; +import com.google.cardboard.sdk.nativetypes.EyeType; +import com.google.cardboard.sdk.nativetypes.Mesh; +import com.google.cardboard.sdk.nativetypes.UvPoint; +import java.util.ArrayList; +import java.util.List; + +/** + * Wrapper around {@code DistortionRenderer}, {@code HeadTracker}, {@code LensDistortion}, {@code + * QrCode} and {@code Initialize}. + */ +public class CardboardViewApi { + /** + * Enum that corresponds to the values of CardboardViewportOrientation in + * google3/third_party/cardboard_oss/sdk/include/cardboard.h + */ + private enum CardboardViewportOrientation { + // Corresponds to CardboardViewportOrientation::kLandscapeLeft + LANDSCAPE_LEFT(0), + // Corresponds to CardboardViewportOrientation::kLandscapeRight + LANDSCAPE_RIGHT(1), + // Corresponds to CardboardViewportOrientation::kPortrait + PORTRAIT(2), + // Corresponds to CardboardViewportOrientation::kPortraitUpsideDown + PORTRAIT_REVERSED(3); + + private final int value; + + private CardboardViewportOrientation(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + } + + /** Holds the screen size information in pixels. */ + public static class ScreenSize { + final int width; + final int height; + + /** + * Constructs a ScreenSize. + * + * @param[in] width The screen width in pixels. It must be positive. + * @param[in] height The screen height in pixels. It must be positive. + */ + ScreenSize(int width, int height) { + this.width = width; + this.height = height; + } + } + + /** Holds device's field of view. */ + public static class FieldOfView { + final float left; + final float top; + final float right; + final float bottom; + + /** + * Constructs a FieldOfView. + * + * @param[in] left The left angle in degrees of the field of view. It must be positive. + * @param[in] right The right angle in degrees of the field of view. It must be positive. + * @param[in] bottom The bottom angle in degrees of the field of view. It must be positive. + * @param[in] top The top angle in degrees of the field of view. It must be positive. + */ + public FieldOfView(float left, float right, float bottom, float top) { + this.left = left; + this.top = top; + this.right = right; + this.bottom = bottom; + } + } + + /** + * Default delta time in nanoseconds to get the future pose estimate from the {@code HeadTracker}. + */ + private static final long PREDICTION_TIME_WITHOUT_VSYNC_NANOS = 50000000; + + /** Default display value to forward to {@code DistortionRenderer}. */ + private static final long NATIVE_TARGET_DISPLAY = 0; + + /** Holds the screen size information. */ + private ScreenSize screenSize; + + /** Holds the {@code DistortionRenderer} object. */ + private DistortionRenderer distortionRenderer; + + /** Holds the {@code LensDistortion} object. */ + private LensDistortion lensDistortion; + + /** Holds the {@code HeadTracker} object. */ + private HeadTracker headTracker; + + /** Holds device's field of view. */ + private FieldOfView fieldOfView; + + /** Delta time in nanoseconds to obtain the pose from {@code HeadTracker}. */ + private long deltaTimeInNs; + + /** The current Context. It is generally an Activity instance or wraps one. */ + private final Context context; + + /** The current display. Used to get the display rotation. */ + private final Display display; + + /** The current display rotation. */ + private int displayRotation; + + /** The cached display rotation from the previous call to {@code getPose()}. */ + private int cachedDisplayRotation; + + /** The current device orientation. */ + private CardboardViewportOrientation deviceOrientation; + + private static final String CARDBOARD_CONFIGURE_ACTION = + "com.google.vrtoolkit.cardboard.CONFIGURE"; + + /** Tag for logging purposes. */ + private static final String TAG = CardboardViewApi.class.getSimpleName(); + + /** + * Constructs a CardboardViewApi. + * + *

It initializes the JNI modules and loads the {@code HeadTracker}. For the pose estimation, a + * default positive delta time is assigned which leads to obtaining a future pose every time it is + * queried. + * + *

Consumers of this object should refer to {@code loadDeviceParams()} and {@code + * initializeRenderThread()} to fully initialize this object with the device (viewer) information. + * + * @param[in] context The current Context. It is generally an Activity instance or wraps one. The + * following is the list of methods that will be queried: getFilesDir(), getPackageManager(), + * getResources(), getSystemService(Context.WINDOW_SERVICE), startActivity(Intent), + * getContentResolver(), getDisplay(). + */ + @SuppressWarnings("deprecation") // This is ignored because our app minSdkVersion is 16. + public CardboardViewApi(Context context) { + // Guarantees proper initialization of the JavaVM and Android context. + Initialize.initialize(context); + + this.context = context; + distortionRenderer = null; + lensDistortion = null; + headTracker = new HeadTracker(); + + screenSize = new ScreenSize(0, 0); + fieldOfView = new FieldOfView(0f, 0f, 0f, 0f); + + deltaTimeInNs = PREDICTION_TIME_WITHOUT_VSYNC_NANOS; + if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) { + DisplayManager displayManager = + (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); + display = displayManager.getDisplay(Display.DEFAULT_DISPLAY); + } else { + // This is necessary because our minimum API level is 16 + display = + ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); + } + displayRotation = 0; + cachedDisplayRotation = -1; + deviceOrientation = CardboardViewportOrientation.LANDSCAPE_LEFT; + } + + /** + * Releases resources taken at construct time and after initializing device parameters. + * + *

Preconditions: it must be called from the rendering thread. + */ + public void close() { + if (distortionRenderer != null) { + distortionRenderer.close(); + distortionRenderer = null; + } + if (lensDistortion != null) { + lensDistortion.close(); + lensDistortion = null; + } + if (headTracker != null) { + headTracker.close(); + headTracker = null; + } + } + + /** + * Forwards a call to {@code close()} and is provided as a safe-guard in case it is not called. + */ + @Override + protected void finalize() throws Throwable { + // TODO(b/146156526): Remove finalize() usage from Java API. + close(); + super.finalize(); + } + + /** + * Sets current screen parameters. + * + * @param[in] width The screen width in pixels. + * @param[in] height The screen height in pixels. + */ + public void setScreenParams(int width, int height) { + screenSize = new ScreenSize(width, height); + } + + /** + * Getter of the configured width property of the screen parameters. + * + * @return The width in pixels of the screen. + */ + public int getScreenParamsWidth() { + return screenSize.width; + } + + /** + * Getter of the configured height property of the screen parameters. + * + * @return The height in pixels of the screen. + */ + public int getScreenParamsHeight() { + return screenSize.height; + } + + /** + * Getter of the device's field of view top angle. + * + * @return The device's field of view top angle + */ + public float getFieldOfViewParamsTop() { + return fieldOfView.top; + } + + /** + * Getter of the device's field of view left angle. + * + * @return The device's field of view left angle. + */ + public float getFieldOfViewParamsLeft() { + return fieldOfView.left; + } + + /** + * Getter of the device's field of view right angle. + * + * @return The device's field of view right angle. + */ + public float getFieldOfViewParamsRight() { + return fieldOfView.right; + } + + /** + * Getter of the device's field of view bottom angle. + * + * @return The device's field of view bottom angle. + */ + public float getFieldOfViewParamsBottom() { + return fieldOfView.bottom; + } + + /** + * Computes the eye from head matrix. + * + *

Preconditions: {@code LensDistortion} must be previously and successfully initialized. When + * it is unmet, a call to this function results in a no-op. + * + * @param[in] eye Desired eye. + * @param[out] eyeFromHeadMatrix 4x4 float eye from head matrix. + * @throws IllegalArgumentException When @p eye is not a valid eye type. + */ + public void getEyeFromHeadMatrix(int eye, float[] eyeFromHeadMatrix) { + if (!isValidEyeType(eye)) { + throw new IllegalArgumentException("eye is invalid."); + } + if (lensDistortion == null) { + Log.w(TAG, "Tried to getEyeFromHeadMatrix but LensDistortion is not initialized."); + return; + } + lensDistortion.getEyeFromHeadMatrix(eye, eyeFromHeadMatrix); + } + + /** + * Computes the projection matrix for an eye. + * + *

Preconditions: {@code LensDistortion} must be previously and successfully initialized. When + * it is unmet, a call to this function results in a no-op. + * + * @param[in] eye Desired eye. + * @param[in] zNear Near clip plane z-axis coordinate. + * @param[in] zFar Far clip plane z-axis coordinate. + * @param[out] projectionMatrix 4x4 float ideal projection matrix. + * @throws IllegalArgumentException When @p eye is not a valid eye type. + */ + public void getEyeProjectionMatrix(int eye, float zNear, float zFar, float[] projectionMatrix) { + if (!isValidEyeType(eye)) { + throw new IllegalArgumentException("eye is invalid."); + } + if (lensDistortion == null) { + Log.w(TAG, "Tried to getEyeProjectionMatrix but LensDistortion is not initialized."); + return; + } + lensDistortion.getEyeProjectionMatrix(eye, zNear, zFar, projectionMatrix); + } + + /** + * Computes the distortion {@code Mesh} for an eye. + * + *

Preconditions: {@code LensDistortion} must be previously and successfully initialized. + * + *

Important: The distorsion {@code Mesh} that is returned by this function becomes invalid if + * {@code LensDistortion} is destroyed. + * + * @param[in] eye Desired eye. + * @return The distortion {@code Mesh}. + * @throws IllegalStateException When this method is called before a successful initialization of + * {@code LensDistortion}. + */ + public Mesh getDistortionMesh(int eye) { + if (lensDistortion == null) { + throw new IllegalStateException("LensDistortion has not been initialized yet."); + } + if (!isValidEyeType(eye)) { + throw new IllegalArgumentException("eye is invalid."); + } + return lensDistortion.getDistortionMesh(eye); + } + + /** + * Undistorts a {@code UvPoint} for an eye. + * + *

Preconditions: {@code LensDistortion} must be previously and successfully initialized. + * + * @param[in] distortedUv A distorted {@code UvPoint} point to undistort. + * @param[in] eye Desired eye. + * @return An undistorted {@code UvPoint}. + * @throws IllegalStateException When this method is called before a successful initialization of + * {@code LensDistortion}. + */ + UvPoint undistortedUvForDistortedUv(UvPoint distortedUv, int eye) { + if (lensDistortion == null) { + throw new IllegalStateException("LensDistortion has not been initialized yet."); + } + if (!isValidEyeType(eye)) { + throw new IllegalArgumentException("eye is invalid."); + } + return lensDistortion.undistortedUvForDistortedUv(distortedUv, eye); + } + + /** + * Distorts a {@code UvPoint} for an eye. + * + *

Preconditions: {@code LensDistortion} must be previously and successfully initialized. + * + * @param[in] undistortedUv An undistorted {@code UvPoint} point to distort. + * @param[in] eye Desired eye. + * @return A distorted {@code UvPoint}. + * @throws IllegalStateException When this method is called before a successful initialization of + * {@code LensDistortion}. + */ + UvPoint distortedUvForUndistortedUv(UvPoint undistortedUv, int eye) { + if (lensDistortion == null) { + throw new IllegalStateException("LensDistortion has not been initialized yet."); + } + if (!isValidEyeType(eye)) { + throw new IllegalArgumentException("eye is invalid."); + } + return lensDistortion.distortedUvForUndistortedUv(undistortedUv, eye); + } + + /** + * Initializes all GL-related modules. + * + *

Preconditions: must be called from the rendering thread. + */ + public void initializeRenderThread() { + if (distortionRenderer != null) { + distortionRenderer.close(); + } + distortionRenderer = new DistortionRenderer(); + } + + /** + * Sets a {@code Mesh} for an eye. + * + *

Preconditions: {@code DistortionRenderer} must be previously and successfully initialized. + * When it is unment, a call to this function results in a no-op. + * + *

Preconditions: must be called from the rendering thread. + * + * @param[in] mesh The {@code Mesh} to set. + * @param[in] eye Desired eye. + */ + public void setMesh(Mesh mesh, int eye) { + if (distortionRenderer == null) { + Log.w(TAG, "Tried to setMesh but DistortionRenderer is not initialized."); + return; + } + if (!isValidEyeType(eye)) { + throw new IllegalArgumentException("eye is invalid."); + } + distortionRenderer.setMesh(mesh, eye); + } + + /** + * Renders left and right eye textures to display. + * + *

Preconditions: {@code DistortionRenderer} must be previously and successfully initialized. + * When it is unment, a call to this function results in a no-op. + * + *

Preconditions: must be called from the rendering thread. + * + * @param[in] leftEyeTextureDescription Left eye texture description. + * @param[in] rightEyeTextureDescription Right eye texture description. + */ + public void renderEyeToDisplay( + EyeTextureDescription leftEyeTextureDescription, + EyeTextureDescription rightEyeTextureDescription) { + if (distortionRenderer == null) { + Log.w(TAG, "Tried to renderEyeToDisplay but DistortionRenderer is not initialized."); + return; + } + // TODO(b/156227563): Unhardcode rect information. + distortionRenderer.renderEyeToDisplay( + NATIVE_TARGET_DISPLAY, + 0, + 0, + screenSize.width, + screenSize.height, + leftEyeTextureDescription, + rightEyeTextureDescription); + } + + /** + * Pauses the {@code HeadTracker} pose tracking. + * + *

Precondition: the {@code HeadTracker} is initialized. When it is unmet, a call to this + * function results in a no-op. + */ + public void pauseHeadTracker() { + if (headTracker != null) { + headTracker.pause(); + } else { + Log.w(TAG, "Tried to pause the HeadTracker but it is not initialized."); + } + } + + /** + * Resumes the {@code HeadTracker} pose tracking. + * + *

Precondition: the {@code HeadTracker} is initialized. When it is unmet, a call to this + * function results in a no-op. + */ + public void resumeHeadTracker() { + if (headTracker != null) { + headTracker.resume(); + } else { + Log.w(TAG, "Tried to resume the HeadTracker but it is not initialized."); + } + } + + /** + * Sets the delta time in nanoseconds used to compute the pose at some point in time with respect + * to the call time of {@code getPose()}. + */ + public void setDeltaTimeInNs(long deltaTimeInNs) { + this.deltaTimeInNs = deltaTimeInNs; + } + + /** + * Gets the value of {@code deltaTimeInNs}. + * + * @return The time in nanoseconds used to compute the pose at some point in time with respect to + * the call time of {@code getPose()}. + */ + public long getDeltaTimeInNs() { + return deltaTimeInNs; + } + + /** + * Gets the pose of the {@code HeadTracker}. + * + *

Precondition: the {@code HeadTracker} is initialized. When it is unmet, a call to this + * function results in a no-op. + * + * @param[out] position 3 floats for (x, y, z). + * @param[out] orientation 4 floats for quaternion. + */ + public void getPose(float[] position, float[] orientation) { + displayRotation = display.getRotation(); + + if (cachedDisplayRotation != displayRotation) { + setDeviceOrientation(displayRotation); + } + + if (headTracker != null) { + headTracker.getPose(deltaTimeInNs, deviceOrientation.getValue(), position, orientation); + } else { + Log.w(TAG, "Tried to get the pose from HeadTracker but it is not initialized."); + } + } + + /** + * Checks for device parameters existence and initializes {@code LensDistortion} module if they + * exist. + * + *

Loading flow: - If the application is run in Android P or below, the READ_EXTERNAL_STORAGE + * permission is checked. If granted, parameters are read from external storage. - If the + * application is run in Android Q or above, device parameters are read from scoped storage. - If + * the previous step failed, it uses Cardboard V1 device parameters. + */ + public void loadDeviceParams() { + byte[] deviceParams = null; + + if ((VERSION.SDK_INT <= VERSION_CODES.P && isReadExternalStoragePermissionEnabled()) + || VERSION.SDK_INT > VERSION_CODES.P) { + deviceParams = QrCode.getSavedDeviceParams(); + } + + if (deviceParams == null) { + deviceParams = QrCode.getCardboardV1DeviceParams(); + } + + initializeFieldOfView(deviceParams); + if (lensDistortion != null) { + lensDistortion.close(); + } + lensDistortion = new LensDistortion(deviceParams, screenSize.width, screenSize.height); + } + + /** + * Checks if a Cardboard viewer has previously been scanned and its parameters saved. + * + * @return true if a Cardboard viewer has previously been scanned and its parameters saved. + */ + public boolean hasSavedDeviceParams() { + if (VERSION.SDK_INT <= VERSION_CODES.P && !isReadExternalStoragePermissionEnabled()) { + return false; + } + return QrCode.getSavedDeviceParams() != null; + } + + /** + * Returns the device parameters of the Cardboard viewer. + * + * @return the device parameters of the Cardboard viewer. + */ + public CardboardDevice.DeviceParams getSavedDeviceParams() { + return DeviceParamsUtils.parseCardboardDeviceParams(QrCode.getSavedDeviceParams()); + } + + /** Scans a Cardboard QR code from a viewer and saves the obtained parameters. */ + public void scanViewerQrCode() { + QrCode.scanQrCodeAndSaveDeviceParams(); + } + + /** Launches VRCore Settings Activity. */ + public void launchVrCoreSettingsActivity() { + PackageManager pm = context.getPackageManager(); + Intent settingsIntent = new Intent(); + settingsIntent.setAction(CARDBOARD_CONFIGURE_ACTION).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + // Grab the settings intent handlers whose package begins with 'com.google.' + // Note that the result should be ordered by *descending* priority, but we make + // no such assumption. + // TODO: b/441496226 - Add declaration to manifest. + List resolveInfos = pm.queryIntentActivities(settingsIntent, 0 /* flags */); + List googleIntentsWithHighestPriority = new ArrayList<>(); + Integer highestGoogleIntentPriority = null; + for (ResolveInfo info : resolveInfos) { + String pkgName = info.activityInfo.packageName; + if (!PackageUtils.isGooglePackage(pkgName)) { + continue; + } + + // With N+, apps are no longer allowed to give >0 priority to intent-filters. + // Prioritizing system apps avoids the disambiguation dialog when + // legacy Cardboard apps (with a priority of 0) are installed alongside a + // system VrCore installation. + int priority = info.priority; + if (PackageUtils.isSystemPackage(context, pkgName)) { + ++priority; + } + + if (highestGoogleIntentPriority == null) { + highestGoogleIntentPriority = priority; + } else if (priority > highestGoogleIntentPriority.intValue()) { + // If the intent has a higher priority, accept it and ignore all lower priority matches. + highestGoogleIntentPriority = priority; + googleIntentsWithHighestPriority.clear(); + } else if (priority < highestGoogleIntentPriority.intValue()) { + // If the intent has a lesser priority, ignore it. + continue; + } + + Intent intent = new Intent(settingsIntent); + intent.setClassName(pkgName, info.activityInfo.name); + googleIntentsWithHighestPriority.add(intent); + } + + if (!googleIntentsWithHighestPriority.isEmpty()) { + // If there is only one matching Google-owned activity with the highest priority, have the + // dialog directly launch it and avoid offering a chooser for any third-party apps that + // intercept the intent. Otherwise, go through intent resolution as usual. Note this may show + // third-party apps that intercept the intent, which is fine since the list is guaranteed to + // also contain at least 2 Google-owned Cardboard configuration Activities. + Intent intent = + googleIntentsWithHighestPriority.size() == 1 + ? googleIntentsWithHighestPriority.get(0) + : settingsIntent; + // Go straight to the activity. + context.startActivity(intent); + } + } + + /** + * Checks if reading external storage permission is granted. + * + * @return whether the permission is already granted. + */ + private boolean isReadExternalStoragePermissionEnabled() { + return ActivityCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) + == PackageManager.PERMISSION_GRANTED; + } + + /** + * Determines whether {@code eye} is equal to either {@code EyeType.LEFT} or {@code + * EyeType.RIGHT}. + * + * @param eye An integer pointing to either the left or right eye. + * @return true When {@code eye} is equal to either {@code EyeType.LEFT} or {@code EyeType.RIGHT}. + */ + private static boolean isValidEyeType(int eye) { + return eye == EyeType.LEFT || eye == EyeType.RIGHT; + } + + /** + * Initializes the field of view form device parameters. + * + *

Left eye field of view is an optional paramater. When it is provided, we set the custom + * device field of view. Otherwise, Cardboard V1 device field of view is set. + * + * @param[in] deviceParamsBuffer A byte buffer with the serialized device parameters. + */ + private void initializeFieldOfView(byte[] deviceParamsBuffer) { + final CardboardDevice.DeviceParams deviceParams = + DeviceParamsUtils.parseCardboardDeviceParams(deviceParamsBuffer); + if (deviceParams.getLeftEyeFieldOfViewAnglesCount() == 4) { + fieldOfView = + new FieldOfView( + deviceParams.getLeftEyeFieldOfViewAngles(0), + deviceParams.getLeftEyeFieldOfViewAngles(1), + deviceParams.getLeftEyeFieldOfViewAngles(2), + deviceParams.getLeftEyeFieldOfViewAngles(3)); + } else { + fieldOfView = + new FieldOfView( + CardboardV1DeviceParams.CARDBOARD_V1_FOV_ANGLES[0], + CardboardV1DeviceParams.CARDBOARD_V1_FOV_ANGLES[1], + CardboardV1DeviceParams.CARDBOARD_V1_FOV_ANGLES[2], + CardboardV1DeviceParams.CARDBOARD_V1_FOV_ANGLES[3]); + } + } + + private void setDeviceOrientation(int displayRotation) { + if (displayRotation == Surface.ROTATION_0) { + // This corresponds to CardboardViewportOrientation::kPortrait + deviceOrientation = CardboardViewportOrientation.PORTRAIT; + } else if (displayRotation == Surface.ROTATION_180) { + // This corresponds to CardboardViewportOrientation::kPortrait + deviceOrientation = CardboardViewportOrientation.PORTRAIT_REVERSED; + } else if (displayRotation == Surface.ROTATION_90) { + // This corresponds to CardboardViewportOrientation::kLandscapeLeft + deviceOrientation = CardboardViewportOrientation.LANDSCAPE_LEFT; + } else if (displayRotation == Surface.ROTATION_270) { + // This corresponds to CardboardViewportOrientation::kLandscapeRight + deviceOrientation = CardboardViewportOrientation.LANDSCAPE_RIGHT; + } + } +} diff --git a/java_api/src/main/java/com/google/cardboard/sdk/DistortionRenderer.java b/java_api/src/main/java/com/google/cardboard/sdk/DistortionRenderer.java new file mode 100644 index 00000000..c6da8293 --- /dev/null +++ b/java_api/src/main/java/com/google/cardboard/sdk/DistortionRenderer.java @@ -0,0 +1,115 @@ +/* + * Copyright 2019 Google LLC + * + * 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.cardboard.sdk; + +import com.google.cardboard.sdk.nativetypes.EyeTextureDescription; +import com.google.cardboard.sdk.nativetypes.Mesh; + +/** Cardboard SDK Distortion Renderer API. */ +public class DistortionRenderer { + + // Opaque native pointer to the native DistortionRenderer instance. + // This object is owned by this class instance and passed to the native methods. + private long nativeDistortionRenderer; + + /** + * Creates a new distortion renderer object. + * + *

Preconditions: Must be called from the rendering thread. + */ + public DistortionRenderer() { + nativeDistortionRenderer = nativeDistortionRendererCreate(); + } + + /** + * Releases memory created at construct time by the native distortion renderer object. This method + * should be executed just once after construct time, then it will not have effect and a new + * instance must be instantiated. If this function is not called, additional resources will be + * consumed. + * + *

Preconditions: Must be called from the rendering thread. + */ + public void close() { + nativeDistortionRendererDestroy(nativeDistortionRenderer); + nativeDistortionRenderer = 0; + } + + /** + * Calls {@code close()} in case that method is mistakenly not called. + * + *

Preconditions: Must be called from the rendering thread. + */ + @Override + protected void finalize() throws Throwable { + // TODO(b/146156526): Remove finalize() usage from Java API. + close(); + super.finalize(); + } + + /** + * Sets Distortion Mesh for a particular eye. + * + *

Preconditions: Must be called from the rendering thread. + * + * @param[in] mesh Distortion mesh. + * @param[in] eye Desired eye. + */ + public void setMesh(Mesh mesh, int eye) { + nativeDistortionRendererSetMesh(nativeDistortionRenderer, mesh, eye); + } + + /** + * Renders eye textures to display. + * + *

Preconditions: Must be called from the rendering thread. + * + * @param[in] targetDisplay Target display. + * @param[in] x x coordinate of the rectangle's lower left corner to draw into. + * @param[in] y y coordinate of the rectangle's lower left corner to draw into. + * @param[in] width Size in pixels of rectangle's width to draw into. + * @param[in] height Size in pixels of rectangle's height to draw into. + * @param[in] leftEye Left eye texture description. + * @param[in] rightEye Right eye texture description. + */ + public void renderEyeToDisplay( + long targetDisplay, + int x, + int y, + int width, + int height, + EyeTextureDescription leftEye, + EyeTextureDescription rightEye) { + nativeDistortionRendererRenderEyeToDisplay( + nativeDistortionRenderer, targetDisplay, x, y, width, height, leftEye, rightEye); + } + + private native long nativeDistortionRendererCreate(); + + private native void nativeDistortionRendererDestroy(long nativeDistortionRenderer); + + private native void nativeDistortionRendererSetMesh( + long nativeDistortionRenderer, Mesh mesh, int eye); + + private native void nativeDistortionRendererRenderEyeToDisplay( + long nativeDistortionRenderer, + long targetDisplay, + int x, + int y, + int width, + int height, + EyeTextureDescription leftEye, + EyeTextureDescription rightEye); +} diff --git a/java_api/src/main/java/com/google/cardboard/sdk/EglFactory.java b/java_api/src/main/java/com/google/cardboard/sdk/EglFactory.java new file mode 100644 index 00000000..409f52a2 --- /dev/null +++ b/java_api/src/main/java/com/google/cardboard/sdk/EglFactory.java @@ -0,0 +1,219 @@ +/* + * Copyright 2016 Google LLC + * + * 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.cardboard.sdk; + +import android.opengl.EGL14; +import android.opengl.GLSurfaceView.EGLContextFactory; +import android.opengl.GLSurfaceView.EGLWindowSurfaceFactory; +import android.os.Build; +import android.util.Log; + +import java.nio.IntBuffer; +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; +import javax.microedition.khronos.egl.EGLSurface; + +/** + * @hide + * Factory class for creating contexts and surfaces. + */ +public class EglFactory implements EGLContextFactory, EGLWindowSurfaceFactory { + private static final String TAG = "GvrEglFactory"; + public static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098; + private static final int EGL_CONTEXT_PRIORITY_LEVEL = 0x3100; + private static final int EGL_CONTEXT_PRIORITY_HIGH = 0x3101; + private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0; + private static final int EGL_CONTEXT_OPENGL_NO_ERROR_KHR = 0x31B3; + private static final int EGL_CONTEXT_FLAGS_KHR = 0x30FC; + private static final int EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR = 0x00000001; + private static final int MIN_REQUIRED_CONTEXT_CLIENT_VERSION = 2; + + private boolean usePriority = false; + private boolean useProtected = false; + private boolean useDebug = false; + private boolean errorReportingEnabled = true; + private int eglContextClientVersion = 2; + private EGLContext sharedContext = EGL10.EGL_NO_CONTEXT; + + public EglFactory() { + } + + /** + * Sets whether priority contexts should be created. + * + *

Disabled by default. This must be called before calling setRenderer. + * + * @param enabled Whether to create priority contexts. + */ + public void setUsePriorityContext(boolean enabled) { + usePriority = enabled; + } + + /** + * Sets whether protected buffers should be used. + * + *

Disabled by default. This must be called before calling setRenderer. + * + * @param enabled Whether to use protected buffers. + * @throws RuntimeException if EGL 1.4 is unavailable. + */ + public void setUseProtectedBuffers(boolean enabled) { + if (enabled && (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1)) { + throw new RuntimeException( + "Protected buffer support requires EGL 1.4, available only on Jelly Bean MR1 and later."); + } + useProtected = enabled; + } + + /** + * Sets whether KHR_debug logging should be enabled during context creation. + * + *

Disabled by default. This must be called before calling setRenderer. + * + * @param enabled Whether to use KHR_debug. + */ + public void setUseDebug(boolean enabled) { + useDebug = enabled; + } + + /** + * Sets up OpenGL context sharing. + * + *

EGL_NO_CONTEXT by default. This must be called before createContext to enable sharing. + * + * @param context The existing EGL context with which to share OpenGL resources. + */ + public void setSharedContext(EGLContext context) { + sharedContext = context; + } + + /** + * Enables or disables error reporting for the created context. + * + *

When set to false, the context will be created with EGL_CONTEXT_OPENGL_NO_ERROR_KHR + * attribute set to true, which will cause all glGetError calls to return GL_NO_ERROR. + * The default is to report errors normally. + * + * @param enabled Whether to enable error reporting. + */ + public void setErrorReportingEnabled(boolean enabled) { + errorReportingEnabled = enabled; + } + + @Override + public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig) { + IntBuffer attribs = IntBuffer.allocate(10); + + attribs.put(EGL_CONTEXT_CLIENT_VERSION); + attribs.put(eglContextClientVersion); + + // Note that Adreno drivers do not require this flag because glEnable(GL_DEBUG_OUTPUT) is + // sufficient, however Mali drivers require the context flag *and* the OpenGL state. + if (useDebug) { + attribs.put(EGL_CONTEXT_FLAGS_KHR); + attribs.put(EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR); + } + + if (usePriority) { + if (Build.FINGERPRINT.contains("generic_x86")) { + Log.w(TAG, "EGL_CONTEXT_PRIORITY_LEVEL not supported on emulators."); + } else { + attribs.put(EGL_CONTEXT_PRIORITY_LEVEL); + attribs.put(EGL_CONTEXT_PRIORITY_HIGH); + } + } + if (useProtected && supportsProtectedContent(egl, display)) { + attribs.put(EGL_PROTECTED_CONTENT_EXT); + attribs.put(EGL14.EGL_TRUE); + } + if (!errorReportingEnabled) { + final String eglExtensionString = egl.eglQueryString(display, EGL10.EGL_EXTENSIONS); + if (eglExtensionString.contains("EGL_KHR_create_context_no_error")) { + attribs.put(EGL_CONTEXT_OPENGL_NO_ERROR_KHR); + attribs.put(EGL14.EGL_TRUE); + } + } + while (attribs.hasRemaining()) { + attribs.put(EGL10.EGL_NONE); + } + + EGLContext context = egl.eglCreateContext(display, eglConfig, sharedContext, attribs.array()); + if ((context == null || context == EGL10.EGL_NO_CONTEXT) + && eglContextClientVersion > MIN_REQUIRED_CONTEXT_CLIENT_VERSION) { + Log.w( + TAG, + "Failed to create EGL context with version " + + eglContextClientVersion + + ", will try " + + MIN_REQUIRED_CONTEXT_CLIENT_VERSION); + + attribs.array()[1] = MIN_REQUIRED_CONTEXT_CLIENT_VERSION; + context = egl.eglCreateContext(display, eglConfig, sharedContext, attribs.array()); + } + + return context; + } + + @Override + public void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context) { + egl.eglDestroyContext(display, context); + } + + @Override + public EGLSurface createWindowSurface( + EGL10 egl, EGLDisplay display, EGLConfig config, Object nativeWindow) { + EGLSurface result = null; + int[] attribsList = null; + if (useProtected && supportsProtectedContent(egl, display)) { + attribsList = new int[] {EGL_PROTECTED_CONTENT_EXT, EGL14.EGL_TRUE, EGL10.EGL_NONE}; + } + try { + result = egl.eglCreateWindowSurface(display, config, nativeWindow, attribsList); + } catch (IllegalArgumentException e) { + // This exception indicates that the surface flinger surface + // is not valid. This can happen if the surface flinger surface has + // been torn down, but the application has not yet been + // notified via SurfaceTexture.Callback.surfaceDestroyed. + // In theory the application should be notified first, + // but in practice sometimes it is not. See b/4588890. + Log.e(TAG, "eglCreateWindowSurface", e); + } + return result; + } + + @Override + public void destroySurface(EGL10 egl, EGLDisplay display, EGLSurface surface) { + egl.eglDestroySurface(display, surface); + } + + /** + * Sets the EGL version to use. + */ + public void setEGLContextClientVersion(int version) { + eglContextClientVersion = version; + } + + /** + * Returns whether the EGL driver supports EGL_EXT_protected_content. + */ + private boolean supportsProtectedContent(EGL10 egl, EGLDisplay display) { + String extensions = egl.eglQueryString(display, EGL10.EGL_EXTENSIONS); + return extensions.contains("EGL_EXT_protected_content"); + } + +} diff --git a/java_api/src/main/java/com/google/cardboard/sdk/FieldOfView.java b/java_api/src/main/java/com/google/cardboard/sdk/FieldOfView.java new file mode 100644 index 00000000..c2e51a6c --- /dev/null +++ b/java_api/src/main/java/com/google/cardboard/sdk/FieldOfView.java @@ -0,0 +1,291 @@ +/* + * Copyright 2024 Google LLC + * + * 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.cardboard.sdk; + +import android.opengl.Matrix; +import java.util.Arrays; +import java.util.List; +import org.jspecify.annotations.Nullable; + +/** + * Encapsulates a field of view composed of 4 half angles (left, right, bottom, top) as would be + * passed to glFrustum. + */ +public class FieldOfView { + + // Default (Cardboard V2.2) half angles in degrees for the maximum field of view. + // A consequence of using left/right terminology over inner/outer is that + // left and right default angles must be identical. + private static final float CARDBOARD_V2_2_MAX_FOV_LEFT_RIGHT = 60.0f; + private static final float CARDBOARD_V2_2_MAX_FOV_BOTTOM = 60.0f; + private static final float CARDBOARD_V2_2_MAX_FOV_TOP = 60.0f; + + // Parameters for Carboard V1.0. + private static final float CARDBOARD_V1_MAX_FOV_LEFT_RIGHT = 40.0f; + private static final float CARDBOARD_V1_MAX_FOV_BOTTOM = 40.0f; + private static final float CARDBOARD_V1_MAX_FOV_TOP = 40.0f; + + private float left; + private float right; + private float bottom; + private float top; + + public FieldOfView() { + left = CARDBOARD_V2_2_MAX_FOV_LEFT_RIGHT; + right = CARDBOARD_V2_2_MAX_FOV_LEFT_RIGHT; + bottom = CARDBOARD_V2_2_MAX_FOV_BOTTOM; + top = CARDBOARD_V2_2_MAX_FOV_TOP; + } + + /** Returns parameters for Cardboard V1.0.0 */ + public static FieldOfView cardboardV1FieldOfView() { + FieldOfView params = new FieldOfView(); + params.setAngles( + CARDBOARD_V1_MAX_FOV_LEFT_RIGHT, + CARDBOARD_V1_MAX_FOV_LEFT_RIGHT, + CARDBOARD_V1_MAX_FOV_BOTTOM, + CARDBOARD_V1_MAX_FOV_TOP); + + return params; + } + + /** + * Creates a new field of view object with the provided params. + * + * @param left The left field of view half-angle in degrees. + * @param right The right field of view half-angle in degrees. + * @param bottom The bottom field of view half-angle in degrees. + * @param top The top field of view half-angle in degrees. + */ + public FieldOfView(float left, float right, float bottom, float top) { + setAngles(left, right, bottom, top); + } + + /** + * Constructs a new field of view object copying the contents from another. + * + * @param other The other FieldOfView to copy from. + */ + public FieldOfView(FieldOfView other) { + copy(other); + } + + /** + * @hide Creates a field of view object from the contents of protocol buffer field. + * @param angles Angle array from protocol buffer field. + * @return The new field of view object or {@code null} in case of error. + */ + public static @Nullable FieldOfView parseFromProtobuf(float[] angles) { + if (angles.length != 4) { + return null; + } + + return new FieldOfView(angles[0], angles[1], angles[2], angles[3]); + } + + /** + * @hide Creates a field of view object from the contents of protocol buffer field. + * @param angles Angle array from protocol buffer field. + * @return The new field of view object or {@code null} in case of error. + */ + public static @Nullable FieldOfView parseFromProtobuf(List angles) { + if (angles.size() != 4) { + return null; + } + + return new FieldOfView(angles.get(0), angles.get(1), angles.get(2), angles.get(3)); + } + + /** + * @hide Encodes the current field of view information as array for protocol buffer field. + * @return Angle array. + */ + public float[] toProtobuf() { + return new float[] {left, right, bottom, top}; + } + + /** + * @hide Encodes the current field of view information as list for protocol buffer field. + * @return Angle list. + */ + public List toProtobufAsList() { + return Arrays.asList(left, right, bottom, top); + } + + /** + * Copies the contents of another FieldOfView into this one. + * + * @param other The FieldOfView object to copy from. + */ + public void copy(FieldOfView other) { + this.left = other.left; + this.right = other.right; + this.bottom = other.bottom; + this.top = other.top; + } + + /** + * Sets the four half-angles of the field of view. + * + * @param left The left field of view half-angle in degrees. + * @param right The right field of view half-angle in degrees. + * @param bottom The bottom field of view half-angle in degrees. + * @param top The top field of view half-angle in degrees. + */ + public void setAngles(float left, float right, float bottom, float top) { + this.left = left; + this.right = right; + this.bottom = bottom; + this.top = top; + } + + /** + * Sets the left field of view half-angle in degrees. + * + * @param left The left field of view half-angle in degrees. + */ + public void setLeft(float left) { + this.left = left; + } + + /** + * Returns the left field of view half-angle in degrees. + * + * @return The left field of view half-angle in degrees. + */ + public float getLeft() { + return left; + } + + /** + * Sets the right field of view half-angle in degrees. + * + * @param right The right field of view half-angle in degrees. + */ + public void setRight(float right) { + this.right = right; + } + + /** + * Returns the right field of view half-angle in degrees. + * + * @return The right field of view half-angle in degrees. + */ + public float getRight() { + return right; + } + + /** + * Sets the bottom field of view half-angle in degrees. + * + * @param bottom The bottom field of view half-angle in degrees. + */ + public void setBottom(float bottom) { + this.bottom = bottom; + } + + /** + * Returns the bottom field of view half-angle in degrees. + * + * @return The bottom field of view half-angle in degrees. + */ + public float getBottom() { + return bottom; + } + + /** + * Sets the top field of view half-angle in degrees. + * + * @param top The top field of view half-angle in degrees. + */ + public void setTop(float top) { + this.top = top; + } + + /** + * Returns the top field of view half-angle in degrees. + * + * @return The top field of view half-angle in degrees. + */ + public float getTop() { + return top; + } + + /** + * Generates a perspective projection matrix from this object. + * + * @param near The near plane. + * @param far The far plane. + * @param perspective The perspective matrix to fill. + * @param offset The offset into the perspective array to write the matrix to. + * @throws IllegalArgumentException If there is not enough space to write the result. + */ + public void toPerspectiveMatrix(float near, float far, float[] perspective, int offset) { + // Ensure the result fits. + if (offset + 16 > perspective.length) { + throw new IllegalArgumentException("Not enough space to write the result"); + } + + float l = (float) (-Math.tan(Math.toRadians(left)) * near); + float r = (float) (Math.tan(Math.toRadians(right)) * near); + float b = (float) (-Math.tan(Math.toRadians(bottom)) * near); + float t = (float) (Math.tan(Math.toRadians(top)) * near); + Matrix.frustumM(perspective, offset, l, r, b, t, near, far); + } + + /** + * Compares this instance with the specified object and indicates if they are equal. + * + * @param other The object to compare this instance with. + * @return {@code true} if the objects are equal, {@code false} otherwise. + */ + // TODO(b/37774152): implement hashCode() (go/equals-hashcode-lsc) + @SuppressWarnings("EqualsHashCode") + @Override + public boolean equals(Object other) { + if (other == null) { + return false; + } + + if (other == this) { + return true; + } + + if (!(other instanceof FieldOfView)) { + return false; + } + + FieldOfView o = (FieldOfView) other; + return (this.left == o.left && right == o.right && bottom == o.bottom && top == o.top); + } + + /** + * Returns a string containing a concise, human-readable description of this object. + * + * @return A printable representation of this object. + */ + @Override + public String toString() { + return new StringBuilder() + .append("{\n") + .append(" left: " + left + ",\n") + .append(" right: " + right + ",\n") + .append(" bottom: " + bottom + ",\n") + .append(" top: " + top + ",\n") + .append("}") + .toString(); + } +} diff --git a/java_api/src/main/java/com/google/cardboard/sdk/HeadTracker.java b/java_api/src/main/java/com/google/cardboard/sdk/HeadTracker.java new file mode 100644 index 00000000..d2e6dc9c --- /dev/null +++ b/java_api/src/main/java/com/google/cardboard/sdk/HeadTracker.java @@ -0,0 +1,89 @@ +/* + * Copyright 2019 Google LLC + * + * 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.cardboard.sdk; + +/** Cardboard SDK Head Tracker API. */ +public class HeadTracker { + + // Opaque native pointer to the native HeadTracker instance. + // This object is owned by this class instance and passed to the native methods. + private long nativeHeadTracker; + + /** Creates a new head tracker object. */ + public HeadTracker() { + nativeHeadTracker = nativeHeadTrackerCreate(); + } + + /** + * Releases memory created at construct time by the native head tracker object. This method should + * be executed just once after construct time, then it will not have effect and a new instance + * must be instantiated. If this function is not called, additional resources will be consumed. + */ + public void close() { + nativeHeadTrackerDestroy(nativeHeadTracker); + nativeHeadTracker = 0; + } + + /** Calls {@code close()} in case that method is mistakenly not called. */ + @Override + protected void finalize() throws Throwable { + // TODO(b/146156526): Remove finalize() usage from Java API. + close(); + super.finalize(); + } + + /** Pauses head tracker and underlying device sensors. */ + public void pause() { + nativeHeadTrackerPause(nativeHeadTracker); + } + + /** Resumes head tracker and underlying device sensors. */ + public void resume() { + nativeHeadTrackerResume(nativeHeadTracker); + } + + /** + * Gets the predicted head pose for a given timestamp. + * + *

Preconditions: position Must not be null. Preconditions: orientation Must not be null. + * + * @param[in] deltaTimeNs Delta time in nanoseconds from current system boot time to obtain the + * pose. + * @param[in] deviceOrientation Orientation of the device. + * @param[out] position 3 floats for (x, y, z). + * @param[out] orientation 4 floats for quaternion. + */ + public void getPose( + long deltaTimeNs, int deviceOrientation, float[] position, float[] orientation) { + nativeHeadTrackerGetPose( + nativeHeadTracker, deltaTimeNs, deviceOrientation, position, orientation); + } + + private native long nativeHeadTrackerCreate(); + + private native void nativeHeadTrackerDestroy(long nativeHeadTracker); + + private native void nativeHeadTrackerPause(long nativeHeadTracker); + + private native void nativeHeadTrackerResume(long nativeHeadTracker); + + private native void nativeHeadTrackerGetPose( + long nativeHeadTracker, + long deltaTimeNs, + int deviceOrientation, + float[] position, + float[] orientation); +} diff --git a/java_api/src/main/java/com/google/cardboard/sdk/HeadTransform.java b/java_api/src/main/java/com/google/cardboard/sdk/HeadTransform.java new file mode 100644 index 00000000..921a136e --- /dev/null +++ b/java_api/src/main/java/com/google/cardboard/sdk/HeadTransform.java @@ -0,0 +1,252 @@ +/* + * Copyright 2024 Google LLC + * + * 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.cardboard.sdk; + +import android.opengl.Matrix; + +/** Describes the head transform independently of any eye parameters. */ +public class HeadTransform { + + /** Epsilon value used to detect Gimbal lock situations when computing Euler angles. */ + private static final float GIMBAL_LOCK_EPSILON = 1e-2f; + + private final float[] headView; + + public HeadTransform() { + headView = new float[16]; + Matrix.setIdentityM(headView, 0); + } + + /** + * @hide Returns the array where the camera to head matrix transform is stored. For internal use + * only. + * @return The array where the headView matrix is stored. + */ + public float[] getHeadView() { + return headView; + } + + /** + * A matrix representing the transform from the camera to the head. + * + *

Head origin is defined as the center point between the two eyes. + * + * @param headView Array where the 4x4 column-major transformation matrix will be written to. + * @param offset Offset in the array where data should be written. + * @throws IllegalArgumentException If there is not enough space to write the result. + */ + public void getHeadView(float[] headView, int offset) { + // Ensure the result fits. + if (offset + 16 > headView.length) { + throw new IllegalArgumentException("Not enough space to write the result"); + } + + System.arraycopy(this.headView, 0, headView, offset, 16); + } + + /** + * Provides the direction the head is looking towards as a 3x1 unit vector. + * + *

Note that in OpenGL the forward vector points into the -Z direction. Make sure to invert it + * if ever used to compute the basis of a right-handed system. + * + * @param forward Array where the forward vector will be written to. + * @param offset Offset in the array where data should be written. + * @throws IllegalArgumentException If there is not enough space to write the result. + */ + public void getForwardVector(float[] forward, int offset) { + // Ensure the result fits. + if (offset + 3 > forward.length) { + throw new IllegalArgumentException("Not enough space to write the result"); + } + + // Same as multiplying the inverse of the rotation component of the matrix by (0, 0, -1, 0). + for (int i = 0; i < 3; ++i) { + forward[i + offset] = -headView[2 + i * 4]; + } + } + + /** + * Provides the upwards direction of the head as a 3x1 unit vector. + * + * @param up Array where the up vector will be written to. + * @param offset Offset in the array where data should be written. + * @throws IllegalArgumentException If there is not enough space to write the result. + */ + public void getUpVector(float[] up, int offset) { + // Ensure the result fits. + if (offset + 3 > up.length) { + throw new IllegalArgumentException("Not enough space to write the result"); + } + + // Same as multiplying the inverse of the rotation component of the matrix by (0, 1, 0, 0). + for (int i = 0; i < 3; ++i) { + up[i + offset] = headView[1 + i * 4]; + } + } + + /** + * Provides the rightwards direction of the head as a 3x1 unit vector. + * + * @param right Array where the right vector will be written to. + * @param offset Offset in the array where data should be written. + * @throws IllegalArgumentException If there is not enough space to write the result. + */ + public void getRightVector(float[] right, int offset) { + // Ensure the result fits. + if (offset + 3 > right.length) { + throw new IllegalArgumentException("Not enough space to write the result"); + } + + // Same as multiplying the inverse of the rotation component of the matrix by (1, 0, 0, 0). + for (int i = 0; i < 3; ++i) { + right[i + offset] = headView[i * 4]; + } + } + + /** + * Provides the quaternion representing the head rotation. + * + * @param quaternion Array where the quaternion (x, y, z, w) will be written to. + * @param offset Offset in the array where data should be written. + * @throws IllegalArgumentException If there is not enough space to write the result. + */ + public void getQuaternion(float[] quaternion, int offset) { + // Ensure the result fits. + if (offset + 4 > quaternion.length) { + throw new IllegalArgumentException("Not enough space to write the result"); + } + + // Based on the code from http://www.cs.ucr.edu/~vbz/resources/quatut.pdf. + float[] m = headView; + float t = m[0] + m[5] + m[10]; + float x; + float y; + float z; + float w; + + if (t >= 0) { + float s = (float) Math.sqrt(t + 1); + w = 0.5f * s; + s = 0.5f / s; + x = (m[9] - m[6]) * s; + y = (m[2] - m[8]) * s; + z = (m[4] - m[1]) * s; + + } else if ((m[0] > m[5]) && (m[0] > m[10])) { + float s = (float) Math.sqrt(1.0f + m[0] - m[5] - m[10]); + x = s * 0.5f; + s = 0.5f / s; + y = (m[4] + m[1]) * s; + z = (m[2] + m[8]) * s; + w = (m[9] - m[6]) * s; + + } else if (m[5] > m[10]) { + float s = (float) Math.sqrt(1.0f + m[5] - m[0] - m[10]); + y = s * 0.5f; + s = 0.5f / s; + x = (m[4] + m[1]) * s; + z = (m[9] + m[6]) * s; + w = (m[2] - m[8]) * s; + + } else { + float s = (float) Math.sqrt(1.0f + m[10] - m[0] - m[5]); + z = s * 0.5f; + s = 0.5f / s; + x = (m[2] + m[8]) * s; + y = (m[9] + m[6]) * s; + w = (m[4] - m[1]) * s; + } + + quaternion[offset + 0] = x; + quaternion[offset + 1] = y; + quaternion[offset + 2] = z; + quaternion[offset + 3] = w; + } + + /** + * Provides the Euler angles representation of the head rotation. + * + *

Use of Euler angles is discouraged as they might be subject to Gimbal lock situations. Use + * quaternions or rotation matrices instead whenever possible. + * + *

The provided values represent the viewport rotation as pitch, yaw and roll angles where the + * matrix R = Rz(roll) * Rx(pitch) * Ry(yaw) represents the full rotation. This rotation matrix + * order ensures both yaw and roll are in the full [-pi, pi] interval. + * + *

The angles are provided in radians, in this order and within the following intervals: + * + *

    + *
  • Pitch (X axis): [-pi/2, pi/2] + *
  • Yaw (Y axis): [-pi, pi] + *
  • Roll (Z axis): [-pi, pi] + *
+ * + *

The X-Y-Z axes are the basis of a right-handed OpenGL-style coordinate system. During Gimbal + * lock this method enforces yaw to 0 and provides a valid roll angle. + * + * @param eulerAngles Array where the 3 angles will be written to. + * @param offset Offset in the array where data should be written. + * @throws IllegalArgumentException If there is not enough space to write the result. + */ + public void getEulerAngles(float[] eulerAngles, int offset) { + // Ensure the result fits. + if (offset + 3 > eulerAngles.length) { + throw new IllegalArgumentException("Not enough space to write the result"); + } + + // Compute pitch. + float pitch = (float) Math.asin(headView[6]); + float yaw; + float roll; + + // Check the absolute value of the pitch cosine to detect Gimbal lock situations. + if (Math.sqrt(1.0f - headView[6] * headView[6]) >= GIMBAL_LOCK_EPSILON) { + + // No Gimbal lock. + yaw = (float) Math.atan2(-headView[2], headView[10]); + roll = (float) Math.atan2(-headView[4], headView[5]); + + } else { + // Gimbal lock detected. + yaw = 0.0f; + roll = (float) Math.atan2(headView[1], headView[0]); + } + + eulerAngles[offset + 0] = -pitch; + eulerAngles[offset + 1] = -yaw; + eulerAngles[offset + 2] = -roll; + } + + /** + * Provides the relative translation of the head as a 3x1 vector. + * + * @param translation Array where the translation vector will be written to. + * @param offset Offset in the array where data should be written. + * @throws IllegalArgumentException If there is not enough space to write the result. + */ + public void getTranslation(float[] translation, int offset) { + // Ensure the result fits. + if (offset + 3 > translation.length) { + throw new IllegalArgumentException("Not enough space to write the result"); + } + + // Same as multiplying the matrix by (0, 0, 0, 1). + for (int i = 0; i < 3; ++i) { + translation[i + offset] = headView[12 + i]; + } + } +} diff --git a/java_api/src/main/java/com/google/cardboard/sdk/Initialize.java b/java_api/src/main/java/com/google/cardboard/sdk/Initialize.java new file mode 100644 index 00000000..dfdce8a3 --- /dev/null +++ b/java_api/src/main/java/com/google/cardboard/sdk/Initialize.java @@ -0,0 +1,50 @@ +/* + * Copyright 2019 Google LLC + * + * 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.cardboard.sdk; + +import android.content.Context; + +/** + * Cardboard SDK Initialize API. + * + *

It is only needed to be called once per Activity. Code within sdk package is not guaranteed to + * work from multiple Activities. Every time an Activity is destroyed where references to these + * classes live, all destroy() methods should be called before de-referencing them. Every time an + * Activity is created and the SDK JNI objects instantiated, a first call to initialize() is needed + * and then other objects can be constructed. + */ +public class Initialize { + static { + System.loadLibrary("cardboard_sdk_jni"); + } + + /** Class only contains static methods. */ + private Initialize() {} + + /** + * Initializes JavaVM and Android context. + * + * @param[in] context The current Context. It is generally an Activity instance or wraps one. The + * following is the list of methods that will be queried: getFilesDir(), + * getResources(), getSystemService(Context.WINDOW_SERVICE), startActivity(Intent), + * getDisplay(). + */ + public static void initialize(Context context) { + nativeInitialize(context); + } + + private static native void nativeInitialize(Context context); +} diff --git a/java_api/src/main/java/com/google/cardboard/sdk/LensDistortion.java b/java_api/src/main/java/com/google/cardboard/sdk/LensDistortion.java new file mode 100644 index 00000000..19541290 --- /dev/null +++ b/java_api/src/main/java/com/google/cardboard/sdk/LensDistortion.java @@ -0,0 +1,149 @@ +/* + * Copyright 2019 Google LLC + * + * 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.cardboard.sdk; + +import androidx.annotation.Nullable; +import com.google.cardboard.sdk.nativetypes.Mesh; +import com.google.cardboard.sdk.nativetypes.UvPoint; + +/** Cardboard SDK Lens Distortion API. */ +public class LensDistortion { + + // Opaque native pointer to the native LensDistortion instance. + // This object is owned by this class instance and passed to the native methods. + private long nativeLensDistortion; + + /** + * Creates a new lens distortion object. + * + *

Preconditions: deviceParams Must not be null. + * + * @param[in] deviceParams The device parameters serialized using cardboard_device.proto. + * @param[in] displayWidth Size in pixels of display width. + * @param[in] displayHeight Size in pixels of display height. + */ + public LensDistortion(byte[] deviceParams, int displayWidth, int displayHeight) { + nativeLensDistortion = nativeLensDistortionCreate(deviceParams, displayWidth, displayHeight); + } + + /** + * Releases memory created at construct time by the native lens distortion object. This method + * should be executed just once after construct time, then it will not have effect and a new + * instance must be instantiated. + */ + public void close() { + nativeLensDistortionDestroy(nativeLensDistortion); + nativeLensDistortion = 0; + } + + /** Calls {@code close()} in case that method is mistakenly not called. */ + @Override + protected void finalize() throws Throwable { + // TODO(b/146156526): Remove finalize() usage from Java API. + close(); + super.finalize(); + } + + /** + * Gets the eyeFromHead matrix for a particular eye. + * + *

Preconditions: eyeFromHeadMatrix Must not be null. + * + * @param[in] eye Desired eye. + * @param[out] eyeFromHeadMatrix 4x4 float eye from head matrix. + */ + public void getEyeFromHeadMatrix(int eye, float[] eyeFromHeadMatrix) { + nativeLensDistortionGetEyeFromHeadMatrix(nativeLensDistortion, eye, eyeFromHeadMatrix); + } + + /** + * Gets the ideal projection matrix for a particular eye. + * + *

Preconditions: z_near and z_far must be postive and z_near must be smaller than z_far. + * Preconditions: projectionMatrix Must not be null. + * + * @param[in] eye Desired eye. + * @param[in] zNear Near clip plane z-axis coordinate. + * @param[in] zFar Far clip plane z-axis coordinate. + * @param[out] projectionMatrix 4x4 float ideal projection matrix. + */ + public void getEyeProjectionMatrix(int eye, float zNear, float zFar, float[] projectionMatrix) { + nativeLensDistortionGetEyeProjectionMatrix( + nativeLensDistortion, eye, zNear, zFar, projectionMatrix); + } + + /** + * Gets the distortion mesh for a particular eye. + * + *

Important: The distorsion {@code Mesh} that is returned by this function becomes invalid if + * {@code LensDistortion} is destroyed. + * + * @param[in] eye Desired eye. + * @return A distorsion mesh. When {@code destroy()} has been called, this method returns null. + */ + @Nullable + public Mesh getDistortionMesh(int eye) { + return nativeLensDistortionGetDistortionMesh(nativeLensDistortion, eye); + } + + /** + * Applies lens inverse distortion function to a point normalized [0,1] in pre-distortion (eye + * texture) space. + * + * @param[in] distortedUv Distorted UV point. + * @param[in] eye Desired eye. + * @return A point normalized [0,1] in the screen post distort space. When {@code destroy()} has + * been called, this method returns null. + */ + @Nullable + public UvPoint undistortedUvForDistortedUv(UvPoint distortedUv, int eye) { + return nativeLensDistortionUndistortedUvForDistortedUv(nativeLensDistortion, distortedUv, eye); + } + + /** + * Applies lens distortion function to a point normalized [0,1] in the screen post-distortion + * space. + * + * @param[in] undistortedUv Undistorted UV point. + * @param[in] eye Desired eye. + * @return A point normalized [0,1] in pre distort space (eye texture space). When {@code + * destroy()} has been called, this method returns null. + */ + @Nullable + public UvPoint distortedUvForUndistortedUv(UvPoint undistortedUv, int eye) { + return nativeLensDistortionDistortedUvForUndistortedUv( + nativeLensDistortion, undistortedUv, eye); + } + + private native long nativeLensDistortionCreate( + byte[] deviceParams, int displayWidth, int displayHeight); + + private native void nativeLensDistortionDestroy(long nativeLensDistortion); + + private native void nativeLensDistortionGetEyeFromHeadMatrix( + long nativeLensDistortion, int eye, float[] eyeFromHeadMatrix); + + private native void nativeLensDistortionGetEyeProjectionMatrix( + long nativeLensDistortion, int eye, float zNear, float zFar, float[] projectionMatrix); + + private native Mesh nativeLensDistortionGetDistortionMesh(long nativeLensDistortion, int eye); + + private native UvPoint nativeLensDistortionUndistortedUvForDistortedUv( + long nativeLensDistortion, UvPoint distortedUv, int eye); + + private native UvPoint nativeLensDistortionDistortedUvForUndistortedUv( + long nativeLensDistortion, UvPoint undistortedUv, int eye); +} diff --git a/java_api/src/main/java/com/google/cardboard/sdk/PackageUtils.java b/java_api/src/main/java/com/google/cardboard/sdk/PackageUtils.java new file mode 100644 index 00000000..3f824c95 --- /dev/null +++ b/java_api/src/main/java/com/google/cardboard/sdk/PackageUtils.java @@ -0,0 +1,63 @@ +/* + * Copyright 2020 Google LLC + * + * 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.cardboard.sdk; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; + +/** + * Helper class for querying Android package properties. + */ +public class PackageUtils { + private static final String GOOGLE_PACKAGE_PREFIX = "com.google."; + + /** Class only contains static methods. */ + private PackageUtils() {} + + /** + * Reports whether a given package is a "Google" package. + * + * @param packageName the name of the package in question + * @return whether the specified package is a "Google" package + */ + public static boolean isGooglePackage(String packageName) { + return packageName != null && packageName.startsWith(GOOGLE_PACKAGE_PREFIX); + } + + /** + * Reports whether a given package is a "system" package. + * + * @param context The current Context. It is generally an Activity instance or wraps one. + * {@code Context.getPackageManager()} will be invoked. + * @param packageName the name of the package in question + * @return whether the specified package is a "system" package + */ + public static boolean isSystemPackage(Context context, String packageName) { + try { + ApplicationInfo applicationInfo = + context.getPackageManager().getApplicationInfo(packageName, 0); + int applicationFlags = applicationInfo != null ? applicationInfo.flags : 0; + if (((applicationFlags & ApplicationInfo.FLAG_SYSTEM) != 0) + || ((applicationFlags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0)) { + return true; + } + } catch (PackageManager.NameNotFoundException e) { + return false; + } + return false; + } +} diff --git a/java_api/src/main/java/com/google/cardboard/sdk/QrCode.java b/java_api/src/main/java/com/google/cardboard/sdk/QrCode.java new file mode 100644 index 00000000..5ff26d26 --- /dev/null +++ b/java_api/src/main/java/com/google/cardboard/sdk/QrCode.java @@ -0,0 +1,61 @@ +/* + * Copyright 2019 Google LLC + * + * 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.cardboard.sdk; + +import androidx.annotation.Nullable; + +/** Cardboard SDK QR Code API. */ +public class QrCode { + + /** Class only contains static methods. */ + private QrCode() {} + + /** + * Gets currently saved device parameters. + * + * @return device parameters serialized using cardboard_device.proto. If there are not saved + * device parameters, this method returns null. + */ + @Nullable + public static byte[] getSavedDeviceParams() { + return nativeQrCodeGetSavedDeviceParams(); + } + + /** + * Gets Cardboard V1 device parameters. + * + *

Note that a call to {@code nativeQrCodeGetCardboardV1DeviceParams()} might lead to a + * RuntimeException (which is intentionally uncaught). When this very unlikely event happens, + * code is thought to be in a severe and faulty state and signalling it within Java provides more + * tools to analyze the call graph. + * + * @return Cardboard V1 device parameters serialized using cardboard_device.proto. + */ + public static byte[] getCardboardV1DeviceParams() { + return nativeQrCodeGetCardboardV1DeviceParams(); + } + + /** Scans a QR code and saves the encoded device parameters. */ + public static void scanQrCodeAndSaveDeviceParams() { + nativeQrCodeScanQrCodeAndSaveDeviceParams(); + } + + private static native byte[] nativeQrCodeGetSavedDeviceParams(); + + private static native byte[] nativeQrCodeGetCardboardV1DeviceParams(); + + private static native void nativeQrCodeScanQrCodeAndSaveDeviceParams(); +} diff --git a/java_api/src/main/java/com/google/cardboard/sdk/Viewport.java b/java_api/src/main/java/com/google/cardboard/sdk/Viewport.java new file mode 100644 index 00000000..1988eda7 --- /dev/null +++ b/java_api/src/main/java/com/google/cardboard/sdk/Viewport.java @@ -0,0 +1,107 @@ +/* + * Copyright 2024 Google LLC + * + * 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.cardboard.sdk; + +import android.opengl.GLES20; + +/** Defines a viewport rectangle. */ +public class Viewport { + public int x; + public int y; + public int width; + public int height; + + /** + * Sets a new viewport based on the provided coordinates and dimensions. + * + * @param x Horizontal pixel coordinate of the bottom-left corner of the viewport. + * @param y Vertical pixel coordinate of the bottom-left corner of the viewport. + * @param width Width of the viewport in pixels. + * @param height Height of the viewport in pixels. + */ + public void setViewport(int x, int y, int width, int height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + /** Sets the current OpenGL viewport based on the current viewport settings. */ + public void setGLViewport() { + GLES20.glViewport(x, y, width, height); + } + + /** Sets the current OpenGL scissor rect based on the current viewport settings. */ + public void setGLScissor() { + GLES20.glScissor(x, y, width, height); + } + + /** + * Stores the viewport parameters into an int array. + * + * @param array Array to store values in. + * @param offset Offset into the array to start writing values in. + * @throws IllegalArgumentException If there is not enough space to write the result. + */ + public void getAsArray(int[] array, int offset) { + // Ensure the result fits. + if (offset + 4 > array.length) { + throw new IllegalArgumentException("Not enough space to write the result"); + } + + array[offset] = x; + array[offset + 1] = y; + array[offset + 2] = width; + array[offset + 3] = height; + } + + /** + * Returns a string containing a concise, human-readable description of this object. + * + * @return A printable representation of this object. + */ + @Override + public String toString() { + return new StringBuilder() + .append("{\n") + .append(" x: " + x + ",\n") + .append(" y: " + y + ",\n") + .append(" width: " + width + ",\n") + .append(" height: " + height + ",\n") + .append("}") + .toString(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Viewport)) { + return false; + } + Viewport other = (Viewport) obj; + return x == other.x && y == other.y && width == other.width && height == other.height; + } + + @Override + public int hashCode() { + return Integer.valueOf(x).hashCode() + ^ Integer.valueOf(y).hashCode() + ^ Integer.valueOf(width).hashCode() + ^ Integer.valueOf(height).hashCode(); + } +} diff --git a/java_api/src/main/java/com/google/cardboard/sdk/nativetypes/EyeTextureDescription.java b/java_api/src/main/java/com/google/cardboard/sdk/nativetypes/EyeTextureDescription.java new file mode 100644 index 00000000..0a13be14 --- /dev/null +++ b/java_api/src/main/java/com/google/cardboard/sdk/nativetypes/EyeTextureDescription.java @@ -0,0 +1,30 @@ +/* + * Copyright 2019 Google LLC + * + * 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.cardboard.sdk.nativetypes; + +/** Cardboard SDK Eye Texture Description type. */ +public class EyeTextureDescription { + // The texture with eye pixels. + public long texture; + // u coordinate of the left side of the eye. + public float leftU; + // u coordinate of the right side of the eye. + public float rightU; + // v coordinate of the top side of the eye. + public float topV; + // v coordinate of the bottom side of the eye. + public float bottomV; +} diff --git a/java_api/src/main/java/com/google/cardboard/sdk/nativetypes/EyeType.java b/java_api/src/main/java/com/google/cardboard/sdk/nativetypes/EyeType.java new file mode 100644 index 00000000..b593cc49 --- /dev/null +++ b/java_api/src/main/java/com/google/cardboard/sdk/nativetypes/EyeType.java @@ -0,0 +1,27 @@ +/* + * Copyright 2019 Google LLC + * + * 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.cardboard.sdk.nativetypes; + +/** Cardboard SDK Eye type. */ +public abstract class EyeType { + + private EyeType() {} + + // Left eye. + public static final int LEFT = 0; + // Right eye. + public static final int RIGHT = 1; +} diff --git a/java_api/src/main/java/com/google/cardboard/sdk/nativetypes/Mesh.java b/java_api/src/main/java/com/google/cardboard/sdk/nativetypes/Mesh.java new file mode 100644 index 00000000..e0d03d67 --- /dev/null +++ b/java_api/src/main/java/com/google/cardboard/sdk/nativetypes/Mesh.java @@ -0,0 +1,39 @@ +/* + * Copyright 2019 Google LLC + * + * 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.cardboard.sdk.nativetypes; + +/** Cardboard SDK Mesh type. */ +public class Mesh { + // Indices buffer opaque native pointer. + public long indices; + // Number of indices. + public int nIndices; + // Vertices buffer opaque native pointer. 2 floats per vertex: x, y. + public long vertices; + // UV coordinates buffer opaque native pointer. 2 floats per uv: u, v. + public long uvs; + // Number of vertices. + public int nVertices; + + /** Creates a new mesh object. */ + public Mesh(long indices, int nIndices, long vertices, long uvs, int nVertices) { + this.indices = indices; + this.nIndices = nIndices; + this.vertices = vertices; + this.uvs = uvs; + this.nVertices = nVertices; + } +} diff --git a/java_api/src/main/java/com/google/cardboard/sdk/nativetypes/UvPoint.java b/java_api/src/main/java/com/google/cardboard/sdk/nativetypes/UvPoint.java new file mode 100644 index 00000000..b1fd1994 --- /dev/null +++ b/java_api/src/main/java/com/google/cardboard/sdk/nativetypes/UvPoint.java @@ -0,0 +1,30 @@ +/* + * Copyright 2019 Google LLC + * + * 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.cardboard.sdk.nativetypes; + +/** Cardboard SDK UV Point type. */ +public class UvPoint { + // u coordinate. + public float u; + // v coordinate. + public float v; + + /** Creates a new UV point object. */ + public UvPoint(float u, float v) { + this.u = u; + this.v = v; + } +} diff --git a/java_api/src/main/java/com/google/cardboard/sdk/utils/MathUtils.java b/java_api/src/main/java/com/google/cardboard/sdk/utils/MathUtils.java new file mode 100644 index 00000000..6d2fc997 --- /dev/null +++ b/java_api/src/main/java/com/google/cardboard/sdk/utils/MathUtils.java @@ -0,0 +1,89 @@ +/* + * Copyright 2019 Google LLC + * + * 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.cardboard.sdk.utils; + +import android.opengl.Matrix; + +/** Math utility functions. */ +public class MathUtils { + + private static final String TAG = MathUtils.class.getSimpleName(); + + /** Class only contains static methods. */ + private MathUtils() {} + + /** + * Gets isometry matrix from pose position and orientation. + * + * @param[in] posePosition Pose position. + * @param[in] poseOrientation Pose orientation. + * @return 4x4 isometry matrix as a float array. + */ + public static float[] getMatrixFromPose(float[] posePosition, float[] poseOrientation) { + float[] translationMatrix = new float[16]; + Matrix.setIdentityM(translationMatrix, 0); + Matrix.translateM(translationMatrix, 0, posePosition[0], posePosition[1], posePosition[2]); + float[] rotationMatrix = MathUtils.getRotationMatrixFromQuaternion(poseOrientation); + float[] result = new float[16]; + Matrix.multiplyMM(result, 0, translationMatrix, 0, rotationMatrix, 0); + return result; + } + + /** + * Gets rotation matrix from quaternion. + * + * @param[in] orientationQuaternion Orientation quaternion. + * @return 4x4 rotation matrix as a float array. + */ + private static float[] getRotationMatrixFromQuaternion(float[] orientationQuaternion) { + float x = orientationQuaternion[0]; + float y = orientationQuaternion[1]; + float z = orientationQuaternion[2]; + float w = orientationQuaternion[3]; + + float xx = 2 * x * x; + float yy = 2 * y * y; + float zz = 2 * z * z; + + float xy = 2 * x * y; + float xz = 2 * x * z; + float yz = 2 * y * z; + + float xw = 2 * x * w; + float yw = 2 * y * w; + float zw = 2 * z * w; + + float[] result = new float[16]; + result[0] = 1 - yy - zz; + result[1] = xy + zw; + result[2] = xz - yw; + result[3] = 0; + result[4] = xy - zw; + result[5] = 1 - xx - zz; + result[6] = yz + xw; + result[7] = 0; + result[8] = xz + yw; + result[9] = yz - xw; + result[10] = 1 - xx - yy; + result[11] = 0; + result[12] = 0; + result[13] = 0; + result[14] = 0; + result[15] = 1; + + return result; + } +} diff --git a/java_api/src/main/jni/cardboard_jni.cc b/java_api/src/main/jni/cardboard_jni.cc new file mode 100644 index 00000000..d800aaed --- /dev/null +++ b/java_api/src/main/jni/cardboard_jni.cc @@ -0,0 +1,425 @@ +/* + * Copyright 2019 Google LLC + * + * 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. + */ + +#include +#include +#include + +#include + +#include +#include "include/cardboard.h" +#include "jni_utils/android/jni_utils.h" + +#define JNI_METHOD(return_type, clazz, method_name) \ + JNIEXPORT return_type JNICALL \ + Java_com_google_cardboard_sdk_##clazz##_##method_name + +namespace { + +JavaVM* java_vm_; +jclass mesh_class_; +jclass uv_point_class_; +jclass eye_texture_description_class_; + +constexpr const char* kConstructorMethod = ""; +constexpr const char* kJniIntSignature = "I"; +constexpr const char* kJniFloatSignature = "F"; +constexpr const char* kJniLongSignature = "J"; + +constexpr const char* kEyeTextureDescriptionTexture = "texture"; +constexpr const char* kEyeTextureDescriptionLeftU = "leftU"; +constexpr const char* kEyeTextureDescriptionRightU = "rightU"; +constexpr const char* kEyeTextureDescriptionTopV = "topV"; +constexpr const char* kEyeTextureDescriptionBottomV = "bottomV"; + +constexpr const char* kUvPointU = "u"; +constexpr const char* kUvPointV = "v"; + +constexpr const char* kMeshIndices = "indices"; +constexpr const char* kMeshNIndices = "nIndices"; +constexpr const char* kMeshVertices = "vertices"; +constexpr const char* kMeshUvs = "uvs"; +constexpr const char* kMeshNVertices = "nVertices"; + +constexpr uint64_t kNanosInSeconds = 1000000000; + +// Type conversion between native code pointers to intptr_t which are implicitly +// converted into jlongs afterwards if required. +// @param ptr A pointer. +// @tparam T Any native type. +// @return The address that @p ptr holds converted to an intptr_t. +template +constexpr intptr_t ToJniPointer(const T* ptr) { + return reinterpret_cast(ptr); +} + +// Finds and creates global references to java classes used within this module. +// TODO(b/180938531): Release these global references. +void LoadJNIResources(JNIEnv* env) { + mesh_class_ = + reinterpret_cast(env->NewGlobalRef(cardboard::jni::LoadJClass( + env, "com/google/cardboard/sdk/nativetypes/Mesh"))); + uv_point_class_ = + reinterpret_cast(env->NewGlobalRef(cardboard::jni::LoadJClass( + env, "com/google/cardboard/sdk/nativetypes/UvPoint"))); + eye_texture_description_class_ = + reinterpret_cast(env->NewGlobalRef(cardboard::jni::LoadJClass( + env, "com/google/cardboard/sdk/nativetypes/EyeTextureDescription"))); +} + +} // anonymous namespace + +extern "C" { + +// Creates a CardboardEyeTextureDescription out of @p eye. +// +// @param env The JNI environment. +// @param eye A Java EyeTextureDescription reference. +// @return A CardboardEyeTextureDescription instance with the same contents than +// @p eye. +CardboardEyeTextureDescription CardboardEyeTextureDescriptionFromJobject( + JNIEnv* env, jobject eye) { + const jlong java_texture = env->GetLongField( + eye, env->GetFieldID(eye_texture_description_class_, + kEyeTextureDescriptionTexture, kJniLongSignature)); + const jfloat java_left_u = env->GetFloatField( + eye, env->GetFieldID(eye_texture_description_class_, + kEyeTextureDescriptionLeftU, kJniFloatSignature)); + const jfloat java_right_u = env->GetFloatField( + eye, env->GetFieldID(eye_texture_description_class_, + kEyeTextureDescriptionRightU, kJniFloatSignature)); + const jfloat java_top_v = env->GetFloatField( + eye, env->GetFieldID(eye_texture_description_class_, + kEyeTextureDescriptionTopV, kJniFloatSignature)); + const jfloat java_bottom_v = env->GetFloatField( + eye, env->GetFieldID(eye_texture_description_class_, + kEyeTextureDescriptionBottomV, kJniFloatSignature)); + return CardboardEyeTextureDescription{ + static_cast(java_texture), static_cast(java_left_u), + static_cast(java_right_u), static_cast(java_top_v), + static_cast(java_bottom_v)}; +} + +// Converts @p eye into a CardboardEye enumeration. +// @param eye It could be 0 or 1 which represents CardboardEye::kLeft and +// CardboardEye::kRight respectively. +// @return CardboardEye::kLeft when @p eye is 0. Otherwise CardboardEye::kRight. +constexpr CardboardEye CardboardEyeFromJint(jint eye) { + return eye == 0 ? kLeft : kRight; +} + +// @return The time since the boot in nano seconds. +int64_t GetBootTimeNano() { + struct timespec res; + clock_gettime(CLOCK_BOOTTIME, &res); + return (res.tv_sec * kNanosInSeconds) + res.tv_nsec; +} + +// Library entry point. +JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void*) { + java_vm_ = vm; + return JNI_VERSION_1_6; +} + +// Initialization API + +JNI_METHOD(void, Initialize, nativeInitialize) +(JNIEnv* env, jobject, jobject context) { + Cardboard_initializeAndroid(java_vm_, context); + LoadJNIResources(env); +} + +// Lens Distortion API + +JNI_METHOD(jlong, LensDistortion, nativeLensDistortionCreate) +(JNIEnv* env, jobject, jbyteArray device_params, jint display_width, + jint display_height) { + jbyte* device_params_array = env->GetByteArrayElements(device_params, 0); + const jsize size = env->GetArrayLength(device_params); + const jlong lens_distortion = ToJniPointer(CardboardLensDistortion_create( + reinterpret_cast(device_params_array), size, + display_width, display_height)); + env->ReleaseByteArrayElements(device_params, device_params_array, 0); + return lens_distortion; +} + +JNI_METHOD(void, LensDistortion, nativeLensDistortionDestroy) +(JNIEnv*, jobject, jlong lens_distortion) { + if (lens_distortion == 0) { + return; + } + CardboardLensDistortion_destroy( + reinterpret_cast(lens_distortion)); +} + +JNI_METHOD(void, LensDistortion, nativeLensDistortionGetEyeFromHeadMatrix) +(JNIEnv* env, jobject, jlong lens_distortion, jint eye, + jfloatArray eye_from_head_matrix) { + if (lens_distortion == 0) { + return; + } + jfloat* eye_from_head_matrix_array = + env->GetFloatArrayElements(eye_from_head_matrix, 0); + CardboardLensDistortion_getEyeFromHeadMatrix( + reinterpret_cast(lens_distortion), + CardboardEyeFromJint(eye), eye_from_head_matrix_array); + env->ReleaseFloatArrayElements(eye_from_head_matrix, + eye_from_head_matrix_array, 0); +} + +JNI_METHOD(void, LensDistortion, nativeLensDistortionGetEyeProjectionMatrix) +(JNIEnv* env, jobject, jlong lens_distortion, jint eye, jfloat z_near, + jfloat z_far, jfloatArray projection_matrix) { + if (lens_distortion == 0) { + return; + } + jfloat* projection_matrix_array = + env->GetFloatArrayElements(projection_matrix, 0); + CardboardLensDistortion_getProjectionMatrix( + reinterpret_cast(lens_distortion), + CardboardEyeFromJint(eye), z_near, z_far, projection_matrix_array); + env->ReleaseFloatArrayElements(projection_matrix, projection_matrix_array, 0); +} + +JNI_METHOD(jobject, LensDistortion, nativeLensDistortionGetDistortionMesh) +(JNIEnv* env, jobject, jlong lens_distortion, jint eye) { + if (lens_distortion == 0) { + return NULL; + } + CardboardMesh mesh; + CardboardLensDistortion_getDistortionMesh( + reinterpret_cast(lens_distortion), + CardboardEyeFromJint(eye), &mesh); + // Get constructor method + constexpr const char* kConstructorSignature = "(JIJJI)V"; + const jmethodID meshConstructor = + env->GetMethodID(mesh_class_, kConstructorMethod, kConstructorSignature); + // Return built object + const jlong java_indices = ToJniPointer(mesh.indices); + const jint java_n_indices = static_cast(mesh.n_indices); + const jlong java_vertices = ToJniPointer(mesh.vertices); + const jlong java_uvs = ToJniPointer(mesh.uvs); + const jint java_n_vertices = static_cast(mesh.n_vertices); + return env->NewObject(mesh_class_, meshConstructor, java_indices, + java_n_indices, java_vertices, java_uvs, + java_n_vertices); +} + +JNI_METHOD(jobject, LensDistortion, + nativeLensDistortionUndistortedUvForDistortedUv) +(JNIEnv* env, jobject, jlong lens_distortion, jobject distorted_uv, + jint eye) { + if (lens_distortion == 0) { + return NULL; + } + // Load native structure + const CardboardUv native_distorted_uv{ + env->GetFloatField( + distorted_uv, + env->GetFieldID(uv_point_class_, kUvPointU, kJniFloatSignature)), + env->GetFloatField( + distorted_uv, + env->GetFieldID(uv_point_class_, kUvPointV, kJniFloatSignature))}; + const CardboardUv native_undistorted_uv = + CardboardLensDistortion_undistortedUvForDistortedUv( + reinterpret_cast(lens_distortion), + &native_distorted_uv, CardboardEyeFromJint(eye)); + // Get constructor method + constexpr const char* kConstructorSignature = "(FF)V"; + const jmethodID uvPointConstructor = env->GetMethodID( + uv_point_class_, kConstructorMethod, kConstructorSignature); + // Return built object + return env->NewObject(uv_point_class_, uvPointConstructor, + static_cast(native_undistorted_uv.u), + static_cast(native_undistorted_uv.v)); +} + +JNI_METHOD(jobject, LensDistortion, + nativeLensDistortionDistortedUvForUndistortedUv) +(JNIEnv* env, jobject, jlong lens_distortion, jobject undistorted_uv, + jint eye) { + if (lens_distortion == 0) { + return NULL; + } + // Load native structure + const CardboardUv native_undistorted_uv{ + env->GetFloatField( + undistorted_uv, + env->GetFieldID(uv_point_class_, kUvPointU, kJniFloatSignature)), + env->GetFloatField( + undistorted_uv, + env->GetFieldID(uv_point_class_, kUvPointV, kJniFloatSignature))}; + const CardboardUv native_distorted_uv = + CardboardLensDistortion_distortedUvForUndistortedUv( + reinterpret_cast(lens_distortion), + &native_undistorted_uv, CardboardEyeFromJint(eye)); + // Get constructor method + constexpr const char* kConstructorSignature = "(FF)V"; + const jmethodID uvPointConstructor = env->GetMethodID( + uv_point_class_, kConstructorMethod, kConstructorSignature); + // Return built object + return env->NewObject(uv_point_class_, uvPointConstructor, + static_cast(native_distorted_uv.u), + static_cast(native_distorted_uv.v)); +} + +// Distortion Renderer API + +JNI_METHOD(jlong, DistortionRenderer, nativeDistortionRendererCreate) +(JNIEnv*, jobject ) { + CardboardOpenGlEsDistortionRendererConfig config{kGlTexture2D}; + return ToJniPointer(CardboardOpenGlEs2DistortionRenderer_create(&config)); +} + +JNI_METHOD(void, DistortionRenderer, nativeDistortionRendererDestroy) +(JNIEnv*, jobject, jlong distortion_renderer) { + if (distortion_renderer == 0) { + return; + } + CardboardDistortionRenderer_destroy( + reinterpret_cast(distortion_renderer)); +} + +JNI_METHOD(void, DistortionRenderer, nativeDistortionRendererSetMesh) +(JNIEnv* env, jobject, jlong distortion_renderer, jobject mesh, jint eye) { + if (distortion_renderer == 0) { + return; + } + // Load native structure + const CardboardMesh native_mesh{ + reinterpret_cast(env->GetLongField( + mesh, env->GetFieldID(mesh_class_, kMeshIndices, kJniLongSignature))), + static_cast(env->GetIntField( + mesh, env->GetFieldID(mesh_class_, kMeshNIndices, kJniIntSignature))), + reinterpret_cast(env->GetLongField( + mesh, + env->GetFieldID(mesh_class_, kMeshVertices, kJniLongSignature))), + reinterpret_cast(env->GetLongField( + mesh, env->GetFieldID(mesh_class_, kMeshUvs, kJniLongSignature))), + static_cast(env->GetIntField( + mesh, + env->GetFieldID(mesh_class_, kMeshNVertices, kJniIntSignature)))}; + CardboardDistortionRenderer_setMesh( + reinterpret_cast(distortion_renderer), + &native_mesh, CardboardEyeFromJint(eye)); +} + +JNI_METHOD(void, DistortionRenderer, nativeDistortionRendererRenderEyeToDisplay) +(JNIEnv* env, jobject, jlong distortion_renderer, jlong target_display, + jint x, jint y, jint width, jint height, jobject left_eye, jobject right_eye) { + if (distortion_renderer == 0) { + return; + } + // Load native structures + CardboardEyeTextureDescription native_left_eye = + CardboardEyeTextureDescriptionFromJobject(env, left_eye); + CardboardEyeTextureDescription native_right_eye = + CardboardEyeTextureDescriptionFromJobject(env, right_eye); + + CardboardDistortionRenderer_renderEyeToDisplay( + reinterpret_cast(distortion_renderer), + static_cast(target_display), x, y, width, height, + &native_left_eye, &native_right_eye); +} + +// Head Tracker API + +JNI_METHOD(jlong, HeadTracker, nativeHeadTrackerCreate) +(JNIEnv*, jobject) { + return ToJniPointer(CardboardHeadTracker_create()); +} + +JNI_METHOD(void, HeadTracker, nativeHeadTrackerDestroy) +(JNIEnv* , jobject , jlong head_tracker) { + if (head_tracker == 0) { + return; + } + CardboardHeadTracker_destroy( + reinterpret_cast(head_tracker)); +} + +JNI_METHOD(void, HeadTracker, nativeHeadTrackerPause) +(JNIEnv*, jobject, jlong head_tracker) { + if (head_tracker == 0) { + return; + } + CardboardHeadTracker_pause( + reinterpret_cast(head_tracker)); +} + +JNI_METHOD(void, HeadTracker, nativeHeadTrackerResume) +(JNIEnv*, jobject , jlong head_tracker) { + if (head_tracker == 0) { + return; + } + CardboardHeadTracker_resume( + reinterpret_cast(head_tracker)); +} + +JNI_METHOD(void, HeadTracker, nativeHeadTrackerGetPose) +(JNIEnv* env, jobject , jlong head_tracker, jlong delta_time_ns, + jint deviceOrientation, jfloatArray position, jfloatArray orientation) { + if (head_tracker == 0) { + return; + } + jfloat* position_array = env->GetFloatArrayElements(position, 0); + jfloat* orientation_array = env->GetFloatArrayElements(orientation, 0); + const int64_t timestamp_ns = GetBootTimeNano() + delta_time_ns; + CardboardHeadTracker_getPose( + reinterpret_cast(head_tracker), timestamp_ns, + static_cast(deviceOrientation), + position_array, orientation_array); + env->ReleaseFloatArrayElements(position, position_array, 0); + env->ReleaseFloatArrayElements(orientation, orientation_array, 0); +} + +// QR Code Scanner API + +JNI_METHOD(jbyteArray, QrCode, nativeQrCodeGetSavedDeviceParams) +(JNIEnv* env, jobject) { + uint8_t* buffer{}; + int size{}; + CardboardQrCode_getSavedDeviceParams(&buffer, &size); + if (size == 0) { + return NULL; + } + jbyteArray device_params = env->NewByteArray(size); + env->SetByteArrayRegion(device_params, 0, size, (jbyte*)buffer); + CardboardQrCode_destroy(buffer); + return device_params; +} + +JNI_METHOD(jbyteArray, QrCode, nativeQrCodeGetCardboardV1DeviceParams) +(JNIEnv* env, jobject) { + uint8_t* buffer{}; + int size{}; + CardboardQrCode_getCardboardV1DeviceParams(&buffer, &size); + if (size == 0) { + cardboard::jni::ThrowJavaRuntimeException( + env, + "An error occured while retrieving Cardboard V1 device parameters."); + return NULL; + } + jbyteArray device_params = env->NewByteArray(size); + env->SetByteArrayRegion(device_params, 0, size, (jbyte*)buffer); + return device_params; +} + +JNI_METHOD(void, QrCode, nativeQrCodeScanQrCodeAndSaveDeviceParams) +(JNIEnv*, jobject ) { CardboardQrCode_scanQrCodeAndSaveDeviceParams(); } + +} // extern "C" diff --git a/java_api/src/main/res/drawable-xxhdpi/cardboard_oss_quantum_ic_close_white_24.png b/java_api/src/main/res/drawable-xxhdpi/cardboard_oss_quantum_ic_close_white_24.png new file mode 100644 index 00000000..d5fd569a Binary files /dev/null and b/java_api/src/main/res/drawable-xxhdpi/cardboard_oss_quantum_ic_close_white_24.png differ diff --git a/java_api/src/main/res/drawable-xxhdpi/cardboard_oss_quantum_ic_settings_white_24.png b/java_api/src/main/res/drawable-xxhdpi/cardboard_oss_quantum_ic_settings_white_24.png new file mode 100644 index 00000000..ac490c09 Binary files /dev/null and b/java_api/src/main/res/drawable-xxhdpi/cardboard_oss_quantum_ic_settings_white_24.png differ diff --git a/java_api/src/main/res/drawable-xxhdpi/rippleable.xml b/java_api/src/main/res/drawable-xxhdpi/rippleable.xml new file mode 100644 index 00000000..261cc2d9 --- /dev/null +++ b/java_api/src/main/res/drawable-xxhdpi/rippleable.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/java_api/src/main/res/layout/cardboard_ui.xml b/java_api/src/main/res/layout/cardboard_ui.xml new file mode 100644 index 00000000..b1b1eacb --- /dev/null +++ b/java_api/src/main/res/layout/cardboard_ui.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + diff --git a/java_api/src/main/res/values/colors.xml b/java_api/src/main/res/values/colors.xml new file mode 100644 index 00000000..f781fef9 --- /dev/null +++ b/java_api/src/main/res/values/colors.xml @@ -0,0 +1,5 @@ + + + #b4b4b4 + #4cFFFFFF + diff --git a/java_api/src/main/res/values/dimens.xml b/java_api/src/main/res/values/dimens.xml new file mode 100644 index 00000000..c51d551a --- /dev/null +++ b/java_api/src/main/res/values/dimens.xml @@ -0,0 +1,5 @@ + + + 24mm + 2dip + \ No newline at end of file diff --git a/java_api/src/main/res/values/strings.xml b/java_api/src/main/res/values/strings.xml new file mode 100644 index 00000000..be5c3c75 --- /dev/null +++ b/java_api/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + + back + settings + diff --git a/java_api/src/main/res/values/styles.xml b/java_api/src/main/res/values/styles.xml new file mode 100644 index 00000000..ac52f1dc --- /dev/null +++ b/java_api/src/main/res/values/styles.xml @@ -0,0 +1,9 @@ + + + + diff --git a/paperscope/AndroidManifest.xml b/paperscope/AndroidManifest.xml new file mode 100644 index 00000000..9ef7c4a7 --- /dev/null +++ b/paperscope/AndroidManifest.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/paperscope/CMakeLists.txt b/paperscope/CMakeLists.txt new file mode 100644 index 00000000..978b6332 --- /dev/null +++ b/paperscope/CMakeLists.txt @@ -0,0 +1,124 @@ +# Copyright 2020 Google LLC +# +# 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 +# +# https://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. + +cmake_minimum_required(VERSION 3.22.1) +project(paperscope VERSION 1.0.0 LANGUAGES CXX) + +include(FetchContent) + +# C++ flags. +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED True) +add_compile_options(-Wall -Wextra) + +# dependencies +find_library(ANDROID_LIB android) +find_library(GLESv2_LIB GLESv2) +find_library(GLESv3_LIB GLESv3) +find_library(LOG_LIB log) + +FetchContent_Declare( + freetype + GIT_REPOSITORY https://gitlab.freedesktop.org/freetype/freetype.git + GIT_TAG VER-2-13-2 +) + +FetchContent_MakeAvailable(freetype) + +set(CARDBOARD_SDK_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../sdk) +set(CARDBOARD_JAVA_API_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../java_api) +set(PAPERSCOPE_LAUNCHER_DIR ${CMAKE_CURRENT_SOURCE_DIR}/paperscope_launcher) + +# Path to the prebuilt Cardboard SDK library extracted by Gradle +set(CARDBOARD_LIB ${CMAKE_CURRENT_SOURCE_DIR}/libraries/jni/${ANDROID_ABI}/libGfxPluginCardboard.so) + +# Generate localization strings +set(GENSTRINGS_SCRIPT "${PAPERSCOPE_LAUNCHER_DIR}/common/tools/genstrings.py") +set(RESOURCE_FILE "${PAPERSCOPE_LAUNCHER_DIR}/resources/strings/en.lproj/cardboard.strings") +set(NAMESPACE "cardboard") +set(OUT_DIR "${PAPERSCOPE_LAUNCHER_DIR}/generated_strings") + +file(MAKE_DIRECTORY ${OUT_DIR}) + +add_custom_command( + OUTPUT + ${OUT_DIR}/${NAMESPACE}_strings.h + ${OUT_DIR}/${NAMESPACE}_strings.cc + COMMAND python3 ${GENSTRINGS_SCRIPT} + --resource_file=${RESOURCE_FILE} + --namespace=${NAMESPACE} + --out_dir=${OUT_DIR} + --language=cc + DEPENDS + ${GENSTRINGS_SCRIPT} + ${RESOURCE_FILE} + COMMENT "Generating ${NAMESPACE}_strings C++ files..." +) + +# Explicitly set this to STATIC so CMake knows how to build it +add_library(cardboard_strings STATIC + ${OUT_DIR}/${NAMESPACE}_strings.cc + ${OUT_DIR}/${NAMESPACE}_strings.h +) +target_include_directories(cardboard_strings PUBLIC ${OUT_DIR}) + +# Sources +file(GLOB PAPERSCOPE_SOURCES "${PAPERSCOPE_LAUNCHER_DIR}/src/*.cc") + +add_library(cardboard_jni SHARED + ${PAPERSCOPE_SOURCES} + ${PAPERSCOPE_LAUNCHER_DIR}/common/strings/localized_strings.cc + ${CARDBOARD_SDK_DIR}/util/matrix_4x4.cc +) + +# Includes +target_include_directories(cardboard_jni PRIVATE + ${PAPERSCOPE_LAUNCHER_DIR}/src + ${PAPERSCOPE_LAUNCHER_DIR}/common/strings + ${CARDBOARD_SDK_DIR} + ${CARDBOARD_SDK_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR}/../third_party/glm +) + +add_library(cardboard_sdk_jni SHARED + ${CARDBOARD_JAVA_API_DIR}/src/main/jni/cardboard_jni.cc + ${CARDBOARD_SDK_DIR}/jni_utils/android/jni_utils.cc +) + +target_include_directories(cardboard_sdk_jni PRIVATE + ${CARDBOARD_SDK_DIR} +) + +# Link the actual Cardboard library and Android system libraries +target_link_libraries(cardboard_sdk_jni PRIVATE + ${CARDBOARD_LIB} + ${ANDROID_LIB} + ${LOG_LIB} + ${GLESv2_LIB} +) + +# Build +target_link_libraries(cardboard_jni PRIVATE + ${ANDROID_LIB} + ${GLESv2_LIB} + ${GLESv3_LIB} + ${LOG_LIB} + ${CARDBOARD_LIB} + cardboard_sdk_jni + cardboard_strings + freetype +) + +target_link_options(cardboard_sdk_jni PRIVATE "-Wl,-z,max-page-size=16384") +target_link_options(cardboard_jni PRIVATE "-Wl,-z,max-page-size=16384") diff --git a/paperscope/DummyManifest.xml b/paperscope/DummyManifest.xml new file mode 100644 index 00000000..9a40236b --- /dev/null +++ b/paperscope/DummyManifest.xml @@ -0,0 +1,3 @@ + + + diff --git a/paperscope/README.md b/paperscope/README.md new file mode 100644 index 00000000..adbfe02e --- /dev/null +++ b/paperscope/README.md @@ -0,0 +1,39 @@ +# Cardboard Demo App (Paperscope) + +This app is the Android demo application for Google Cardboard. It is based on the public [Cardboard Demo App](https://play.google.com/store/apps/details?id=com.google.samples.apps.cardboarddemo) available on the Google Play Store, but without the Exhibit demo. + +## Features + +The Demo App showcases various capabilities of the Cardboard SDK, such as: + +* **Photosphere**: Immersive 360-degree photo viewer. +* **Magic Window**: A window into a virtual world using the device's sensors. +* **My Videos**: Media viewer for immersive/stereoscopic video content. + +## Requirements + +* **OpenJDK 17** (`openjdk@17`) +* **Android Studio** or Gradle +* **ADB** (Android Debug Bridge) available in your `PATH` +* A physical Android device (recommended for optimal head tracking and VR rendering) + +## Building and Running + +### Using Android Studio +1. Open Android Studio. +2. Select **Open an existing project** and choose the directory containing the cloned Cardboard repository. +3. Wait for the project to sync with the Gradle files. +4. From the run configurations dropdown, select the **paperscope** configuration. +5. Connect your Android device via USB (with USB Debugging enabled). +6. Click **Run** or press `Shift+F10` to install and launch the app on your device. + +### Using Gradle (Command Line) +To build and install the app using the Gradle wrapper from the command line, +run: + +```shell +./gradlew :sdk:assembleDebug :paperscope:assembleDebug +adb install -r paperscope/build/outputs/apk/debug/paperscope-debug.apk +``` + +Make sure `adb` is connected and authorized on your device. diff --git a/paperscope/about/AboutActivity.java b/paperscope/about/AboutActivity.java new file mode 100644 index 00000000..66fb942a --- /dev/null +++ b/paperscope/about/AboutActivity.java @@ -0,0 +1,151 @@ +package com.google.vr.cardboard.paperscope.demo.about; + +import com.google.vr.cardboard.paperscope.demo.R; + +import static com.google.common.base.Preconditions.checkNotNull; + +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.TextView; +import android.widget.Toast; +import com.google.common.flogger.FluentLogger; + +/** Activity for the About page. */ +public class AboutActivity extends AppCompatActivity { + + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + private static final String MARKET_DETAIL = "market://details?id="; + + private static final String PLAYSTORE_LISTING = "http://play.google.com/store/apps/details?id="; + + private static final String PROD_PACKAGE_NAME = "com.google.samples.apps.cardboarddemo"; + + private static final Uri GOOGLE_TOS_URL = Uri.parse("http://www.google.com/accounts/TOS"); + + private static final Uri CARDBOARD_SAFETY_INFORMATION_URL = + Uri.parse("http://www.google.com/get/cardboard/product-safety/"); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.about); + + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(true); + } + // Rate the app. + checkNotNull((View) findViewById(R.id.rate_the_app)) + .setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View v) { + // First try to go directly to Play Store page using a playstore specific intent. + // We don't use getPackageName() here since the dev package name is not present + // in Play Store. + Uri uri = Uri.parse(MARKET_DETAIL + PROD_PACKAGE_NAME); + Intent goToPlayStore = new Intent(Intent.ACTION_VIEW, uri); + try { + startActivity(goToPlayStore); + } catch (ActivityNotFoundException e) { + // If that fails, fall back to a broader view webpage intent. + openWebPageOrShowErrorToast( + AboutActivity.this, Uri.parse(PLAYSTORE_LISTING + PROD_PACKAGE_NAME)); + } + } + }); + + // Terms of service. + checkNotNull((View) findViewById(R.id.terms_of_service)) + .setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View v) { + openWebPageOrShowErrorToast(AboutActivity.this, GOOGLE_TOS_URL); + } + }); + + // Safety. + checkNotNull((View) findViewById(R.id.product_safety)) + .setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View v) { + openWebPageOrShowErrorToast(AboutActivity.this, CARDBOARD_SAFETY_INFORMATION_URL); + } + }); + + // Set app version. + TextView appVersionString = checkNotNull((TextView) findViewById(R.id.app_version)); + appVersionString.setText(getVersionName()); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + // This is the "Up" button + finish(); + return true; // Event is handled + } + return super.onOptionsItemSelected(item); + } + + /** Opens up the given web page or shows a toast on error. */ + public void openWebPageOrShowErrorToast(Context context, Uri uri) { + try { + context.startActivity(new Intent(Intent.ACTION_VIEW, uri)); + } catch (ActivityNotFoundException e) { + Toast.makeText(context, context.getString(R.string.view_web_page_failed), Toast.LENGTH_SHORT) + .show(); + } + } + + /** + * Returns the version name of the application, or an empty string on error. For dogfood or debug + * builds, this method truncates the version name to the "X.Y.Z" pattern, removing any additional + * suffixes. + */ + private String getVersionName() { + String appVersion = ""; + String packageName = getPackageName(); + PackageManager pm = getPackageManager(); + try { + PackageInfo info; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + info = pm.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0)); + } else { + info = pm.getPackageInfo(packageName, 0); + } + if (info != null) { + appVersion = info.versionName; + } + } catch (PackageManager.NameNotFoundException e) { + logger.atSevere().withCause(e).log("Failed to get package info for %s", packageName); + } + + // Production version strings follow a traditional "X.Y.Z" pattern. + // Dogfood / debug builds follow the following pattern: + // "X.Y.Z.Dogfood long_string_such_as_release_branch_name". + // In order to display something that doesn't overflow for these builds, we strip out everything + // after the first whitespace, if any. + // Note that the full version name can always be accessed from the Android "App info" page. + int spaceIndex = appVersion.indexOf(" "); + if (spaceIndex == -1) { + return appVersion; + } + return appVersion.substring(0, spaceIndex); + } +} diff --git a/paperscope/about/AndroidManifest.xml b/paperscope/about/AndroidManifest.xml new file mode 100644 index 00000000..a9c04324 --- /dev/null +++ b/paperscope/about/AndroidManifest.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/paperscope/about/res/layout/about.xml b/paperscope/about/res/layout/about.xml new file mode 100644 index 00000000..aa4b82fe --- /dev/null +++ b/paperscope/about/res/layout/about.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/paperscope/assets/IconMesh.obj b/paperscope/assets/IconMesh.obj new file mode 100644 index 00000000..d9bf27bb --- /dev/null +++ b/paperscope/assets/IconMesh.obj @@ -0,0 +1,15 @@ +# Blender 4.2.2 LTS +# www.blender.org +o Icon_mesh +v 0.350000 -0.350000 0.000000 +v -0.350000 -0.350000 0.000000 +v 0.350000 0.350000 -0.000000 +v -0.350000 0.350000 -0.000000 +vn -0.0000 -0.0000 -1.0000 +vt 1.000000 0.000000 +vt 0.000000 1.000000 +vt 0.000000 0.000000 +vt 1.000000 1.000000 +s 0 +f 2/1/1 3/2/1 1/3/1 +f 2/1/1 4/4/1 3/2/1 diff --git a/paperscope/assets/Roboto-Regular.ttf b/paperscope/assets/Roboto-Regular.ttf new file mode 100644 index 00000000..4c420003 Binary files /dev/null and b/paperscope/assets/Roboto-Regular.ttf differ diff --git a/paperscope/assets/Tuffy.ttf b/paperscope/assets/Tuffy.ttf new file mode 100644 index 00000000..ade553a6 Binary files /dev/null and b/paperscope/assets/Tuffy.ttf differ diff --git a/paperscope/assets/World.obj b/paperscope/assets/World.obj new file mode 100644 index 00000000..f4dd61d5 --- /dev/null +++ b/paperscope/assets/World.obj @@ -0,0 +1,2039 @@ +# Blender 4.2.2 LTS +# www.blender.org +o Sphere +v 34.047318 83.618576 -28.569096 +v 50.955414 61.546619 -42.756672 +v 60.106007 32.708229 -50.434933 +v 61.283554 17.101002 -51.423012 +v 60.106007 1.493776 -50.434933 +v 50.955414 -27.344616 -42.756672 +v 13.683271 95.563828 -7.506901 +v 26.840706 91.011368 -14.725317 +v 38.966663 83.618576 -21.377846 +v 49.595150 73.669540 -27.208832 +v 58.317726 61.546619 -31.994205 +v 64.799187 47.715679 -35.550053 +v 68.790451 32.708229 -37.739735 +v 70.138138 17.101002 -38.479103 +v 68.790451 1.493776 -37.739735 +v 64.799187 -13.513676 -35.550053 +v 58.317726 -27.344616 -31.994205 +v 49.595150 -39.467541 -27.208832 +v 38.966663 -49.416569 -21.377846 +v 26.840706 -56.809361 -14.725317 +v 13.683271 -61.361820 -7.506901 +v 14.884875 95.563828 -4.693184 +v 29.197735 91.011368 -9.206014 +v 42.388538 83.618576 -13.365054 +v 53.950375 73.669540 -17.010490 +v 63.438927 61.546619 -20.002216 +v 70.489563 47.715679 -22.225279 +v 74.831322 32.708229 -23.594225 +v 76.297348 17.101002 -24.056463 +v 74.831322 1.493776 -23.594225 +v 70.489563 -13.513676 -22.225279 +v 63.438927 -27.344616 -20.002216 +v 53.950375 -39.467541 -17.010490 +v 42.388538 -49.416569 -13.365054 +v 29.197735 -56.809361 -9.206014 +v 14.884875 -61.361820 -4.693184 +v 15.514460 95.563828 -1.699110 +v 30.432713 91.011368 -3.332925 +v 44.181442 83.618576 -4.838654 +v 56.232319 73.669540 -6.158440 +v 66.122200 61.546619 -7.241560 +v 73.471062 47.715679 -8.046392 +v 77.996460 32.708229 -8.542004 +v 79.524506 17.101002 -8.709347 +v 77.996460 1.493776 -8.542004 +v 73.471062 -13.513676 -8.046392 +v 66.122200 -27.344616 -7.241560 +v 56.232319 -39.467541 -6.158440 +v 44.181442 -49.416569 -4.838654 +v 30.432713 -56.809361 -3.332925 +v 15.514460 -61.361820 -1.699110 +v 15.547832 95.563828 1.360259 +v 30.498175 91.011368 2.668244 +v 44.276482 83.618576 3.873693 +v 56.353283 73.669540 4.930271 +v 66.264435 61.546619 5.797390 +v 73.629105 47.715679 6.441704 +v 78.164246 32.708229 6.838482 +v 79.695564 17.101002 6.972466 +v 78.164246 1.493776 6.838482 +v 73.629105 -13.513676 6.441704 +v 66.264435 -27.344616 5.797390 +v 56.353283 -39.467541 4.930271 +v 44.276482 -49.416569 3.873693 +v 30.498175 -56.809361 2.668244 +v 15.547832 -61.361820 1.360259 +v 14.983713 95.563828 4.367354 +v 29.391613 91.011368 8.566874 +v 42.670006 83.618576 12.437172 +v 54.308620 73.669540 15.829517 +v 63.860172 61.546619 18.613546 +v 70.957626 47.715679 20.682255 +v 75.328217 32.708229 21.956173 +v 76.803970 17.101002 22.386328 +v 75.328217 1.493776 21.956173 +v 70.957626 -13.513676 20.682255 +v 63.860172 -27.344616 18.613546 +v 54.308620 -39.467541 15.829517 +v 42.670006 -49.416569 12.437172 +v 29.391613 -56.809361 8.566874 +v 14.983713 -61.361820 4.367354 +v 0.000000 97.100998 0.000000 +v 13.843775 95.563828 7.206614 +v 27.155546 91.011368 14.136281 +v 39.423740 83.618576 20.522701 +v 50.176910 73.669540 26.120441 +v 59.001793 61.546619 30.714394 +v 65.559288 47.715679 34.127998 +v 69.597359 32.708229 36.230095 +v 70.960846 17.101002 36.939888 +v 69.597359 1.493776 36.230095 +v 65.559288 -13.513676 34.127998 +v 59.001793 -27.344616 30.714394 +v 50.176910 -39.467541 26.120441 +v 39.423740 -49.416569 20.522701 +v 27.155546 -56.809361 14.136281 +v 13.843775 -61.361820 7.206614 +v 12.171830 95.563828 9.768927 +v 23.875908 91.011368 19.162441 +v 34.662445 83.618576 27.819553 +v 44.116928 73.669540 35.407574 +v 51.876007 61.546619 41.634899 +v 57.641541 47.715679 46.262222 +v 61.191925 32.708229 49.111717 +v 62.390739 17.101002 50.073875 +v 61.191925 1.493776 49.111717 +v 57.641541 -13.513676 46.262222 +v 51.876007 -27.344616 41.634899 +v 44.116928 -39.467541 35.407574 +v 34.662445 -49.416569 27.819553 +v 23.875908 -56.809361 19.162441 +v 12.171830 -61.361820 9.768927 +v 10.032128 95.563828 11.955826 +v 19.678730 91.011368 23.452200 +v 28.569086 83.618576 34.047318 +v 36.361557 73.669540 43.334011 +v 42.756657 61.546619 50.955406 +v 47.508667 47.715679 56.618610 +v 50.434921 32.708229 60.106003 +v 51.422985 17.101002 61.283543 +v 50.434921 1.493776 60.106003 +v 47.508667 -13.513676 56.618610 +v 42.756657 -27.344616 50.955406 +v 36.361557 -39.467541 43.334011 +v 28.569086 -49.416569 34.047318 +v 19.678730 -56.809361 23.452200 +v 10.032128 -61.361820 11.955826 +v 7.506898 95.563828 13.683268 +v 14.725313 91.011368 26.840704 +v 21.377838 83.618576 38.966656 +v 27.208836 73.669540 49.595150 +v 31.994190 61.546619 58.317719 +v 35.550053 47.715679 64.799179 +v 37.739727 32.708229 68.790443 +v 38.479080 17.101002 70.138123 +v 37.739727 1.493776 68.790443 +v 35.550053 -13.513676 64.799179 +v 31.994190 -27.344616 58.317719 +v 27.208836 -39.467541 49.595150 +v 21.377838 -49.416569 38.966656 +v 14.725313 -56.809361 26.840704 +v 7.506898 -61.361820 13.683268 +v 4.693181 95.563828 14.884871 +v 9.206010 91.011368 29.197731 +v 13.365048 83.618576 42.388531 +v 17.010490 73.669540 53.950367 +v 20.002211 61.546619 63.438919 +v 22.225279 47.715679 70.489555 +v 23.594215 32.708229 74.831314 +v 24.056442 17.101002 76.297333 +v 23.594215 1.493776 74.831314 +v 22.225279 -13.513676 70.489555 +v 20.002211 -27.344616 63.438919 +v 17.010490 -39.467541 53.950367 +v 13.365048 -49.416569 42.388531 +v 9.206010 -56.809361 29.197731 +v 4.693181 -61.361820 14.884871 +v 1.699109 95.563828 15.514455 +v 3.332922 91.011368 30.432707 +v 4.838650 83.618576 44.181435 +v 6.158442 73.669540 56.232311 +v 7.241556 61.546619 66.122192 +v 8.046392 47.715679 73.471054 +v 8.541996 32.708229 77.996445 +v 8.709332 17.101002 79.524475 +v 8.541996 1.493776 77.996445 +v 8.046392 -13.513676 73.471054 +v 7.241556 -27.344616 66.122192 +v 6.158442 -39.467541 56.232311 +v 4.838650 -49.416569 44.181435 +v 3.332922 -56.809361 30.432707 +v 1.699109 -61.361820 15.514455 +v -1.360259 95.563828 15.547828 +v -2.668245 91.011368 30.498169 +v -3.873697 83.618576 44.276474 +v -4.930271 73.669540 56.353275 +v -5.797392 61.546619 66.264427 +v -6.441704 47.715679 73.629097 +v -6.838490 32.708229 78.164230 +v -6.972477 17.101002 79.695541 +v -6.838490 1.493776 78.164230 +v -6.441704 -13.513676 73.629097 +v -5.797392 -27.344616 66.264427 +v -4.930271 -39.467541 56.353275 +v -3.873697 -49.416569 44.276474 +v -2.668245 -56.809361 30.498169 +v -1.360259 -61.361820 15.547828 +v -4.367352 95.563828 14.983707 +v -8.566875 91.011368 29.391609 +v -12.437177 83.618576 42.669991 +v -15.829517 73.669540 54.308617 +v -18.613544 61.546619 63.860161 +v -20.682253 47.715679 70.957619 +v -21.956171 32.708229 75.328201 +v -22.386332 17.101002 76.803940 +v -21.956171 1.493776 75.328201 +v -20.682253 -13.513676 70.957619 +v -18.613544 -27.344616 63.860161 +v -15.829517 -39.467541 54.308617 +v -12.437177 -49.416569 42.669991 +v -8.566875 -56.809361 29.391609 +v -4.367352 -61.361820 14.983707 +v -7.206611 95.563828 13.843771 +v -14.136282 91.011368 27.155540 +v -20.522699 83.618576 39.423729 +v -26.120441 73.669540 50.176903 +v -30.714390 61.546619 59.001785 +v -34.127995 47.715679 65.559280 +v -36.230095 32.708229 69.597351 +v -36.939888 17.101002 70.960815 +v -36.230095 1.493776 69.597351 +v -34.127995 -13.513676 65.559280 +v -30.714390 -27.344616 59.001785 +v -26.120441 -39.467541 50.176903 +v -20.522699 -49.416569 39.423729 +v -14.136282 -56.809361 27.155540 +v -7.206611 -61.361820 13.843771 +v -9.768923 95.563828 12.171825 +v -19.162441 91.011368 23.875900 +v -27.819546 83.618576 34.662430 +v -35.407574 73.669540 44.116924 +v -41.634899 61.546619 51.875999 +v -46.262215 47.715679 57.641537 +v -49.111710 32.708229 61.191914 +v -50.073868 17.101002 62.390709 +v -49.111710 1.493776 61.191914 +v -46.262215 -13.513676 57.641537 +v -41.634899 -27.344616 51.875999 +v -35.407574 -39.467541 44.116924 +v -27.819546 -49.416569 34.662430 +v -19.162441 -56.809361 23.875900 +v -9.768923 -61.361820 12.171825 +v -11.955820 95.563828 10.032125 +v -23.452196 91.011368 19.678726 +v -34.047306 83.618576 28.569075 +v -43.334007 73.669540 36.361553 +v -50.955399 61.546619 42.756649 +v -56.618607 47.715679 47.508656 +v -60.105995 32.708229 50.434910 +v -61.283527 17.101002 51.422958 +v -60.105995 1.493776 50.434910 +v -56.618607 -13.513676 47.508656 +v -50.955399 -27.344616 42.756649 +v -43.334007 -39.467541 36.361553 +v -34.047306 -49.416569 28.569075 +v -23.452196 -56.809361 19.678726 +v -11.955820 -61.361820 10.032125 +v -13.683263 95.563828 7.506895 +v -26.840700 91.011368 14.725308 +v -38.966644 83.618576 21.377827 +v -49.595146 73.669540 27.208830 +v -58.317715 61.546619 31.994186 +v -64.799171 47.715679 35.550049 +v -68.790436 32.708229 37.739716 +v -70.138100 17.101002 38.479053 +v -68.790436 1.493776 37.739716 +v -64.799171 -13.513676 35.550049 +v -58.317715 -27.344616 31.994186 +v -49.595146 -39.467541 27.208830 +v -38.966644 -49.416569 21.377827 +v -26.840700 -56.809361 14.725308 +v -13.683263 -61.361820 7.506895 +v -14.884865 95.563828 4.693180 +v -29.197727 91.011368 9.206006 +v -42.388519 83.618576 13.365041 +v -53.950363 73.669540 17.010487 +v -63.438911 61.546619 20.002205 +v -70.489540 47.715679 22.225275 +v -74.831299 32.708229 23.594206 +v -76.297310 17.101002 24.056421 +v -74.831299 1.493776 23.594206 +v -70.489540 -13.513676 22.225275 +v -63.438911 -27.344616 20.002205 +v -53.950363 -39.467541 17.010487 +v -42.388519 -49.416569 13.365041 +v -29.197727 -56.809361 9.206006 +v -14.884865 -61.361820 4.693180 +v -15.514451 95.563828 1.699109 +v -30.432703 91.011368 3.332921 +v -44.181419 83.618576 4.838644 +v -56.232307 73.669540 6.158438 +v -66.122185 61.546619 7.241550 +v -73.471039 47.715679 8.046396 +v -77.996445 32.708229 8.541988 +v -79.524452 17.101002 8.709312 +v -77.996445 1.493776 8.541988 +v -73.471039 -13.513676 8.046396 +v -66.122185 -27.344616 7.241550 +v -56.232307 -39.467541 6.158438 +v -44.181419 -49.416569 4.838644 +v -30.432703 -56.809361 3.332921 +v -15.514451 -61.361820 1.699109 +v -15.547823 95.563828 -1.360258 +v -30.498165 91.011368 -2.668245 +v -44.276459 83.618576 -3.873697 +v -56.353271 73.669540 -4.930271 +v -66.264420 61.546619 -5.797394 +v -73.629089 47.715679 -6.441704 +v -78.164215 32.708229 -6.838493 +v -79.695511 17.101002 -6.972481 +v -78.164215 1.493776 -6.838493 +v -73.629089 -13.513676 -6.441704 +v -66.264420 -27.344616 -5.797394 +v -56.353271 -39.467541 -4.930271 +v -44.276459 -49.416569 -3.873697 +v -30.498165 -56.809361 -2.668245 +v -15.547823 -61.361820 -1.360258 +v 0.000000 -62.898998 0.000000 +v -14.983704 95.563828 -4.367352 +v -29.391602 91.011368 -8.566873 +v -42.669975 83.618576 -12.437175 +v -54.308609 73.669540 -15.829515 +v -63.860153 61.546619 -18.613546 +v -70.957611 47.715679 -20.682251 +v -75.328186 32.708229 -21.956177 +v -76.803909 17.101002 -22.386332 +v -75.328186 1.493776 -21.956177 +v -70.957611 -13.513676 -20.682251 +v -63.860153 -27.344616 -18.613546 +v -54.308609 -39.467541 -15.829515 +v -42.669975 -49.416569 -12.437175 +v -29.391602 -56.809361 -8.566873 +v -14.983704 -61.361820 -4.367352 +v -13.843766 95.563828 -7.206610 +v -27.155537 91.011368 -14.136279 +v -39.423714 83.618576 -20.522694 +v -50.176895 73.669540 -26.120438 +v -59.001770 61.546619 -30.714388 +v -65.559265 47.715679 -34.127991 +v -69.597336 32.708229 -36.230103 +v -70.960785 17.101002 -36.939884 +v -69.597336 1.493776 -36.230103 +v -65.559265 -13.513676 -34.127991 +v -59.001770 -27.344616 -30.714388 +v -50.176895 -39.467541 -26.120438 +v -39.423714 -49.416569 -20.522694 +v -27.155537 -56.809361 -14.136279 +v -13.843766 -61.361820 -7.206610 +v -12.171822 95.563828 -9.768921 +v -23.875898 91.011368 -19.162437 +v -34.662415 83.618576 -27.819540 +v -44.116920 73.669540 -35.407574 +v -51.875984 61.546619 -41.634895 +v -57.641525 47.715679 -46.262211 +v -61.191902 32.708229 -49.111713 +v -62.390678 17.101002 -50.073853 +v -61.191902 1.493776 -49.111713 +v -57.641525 -13.513676 -46.262211 +v -51.875984 -27.344616 -41.634895 +v -44.116920 -39.467541 -35.407574 +v -34.662415 -49.416569 -27.819540 +v -23.875898 -56.809361 -19.162437 +v -12.171822 -61.361820 -9.768921 +v -10.032122 95.563828 -11.955818 +v -19.678720 91.011368 -23.452192 +v -28.569061 83.618576 -34.047298 +v -36.361549 73.669540 -43.334003 +v -42.756638 61.546619 -50.955395 +v -47.508648 47.715679 -56.618599 +v -50.434895 32.708229 -60.105995 +v -51.422935 17.101002 -61.283520 +v -50.434895 1.493776 -60.105995 +v -47.508648 -13.513676 -56.618599 +v -42.756638 -27.344616 -50.955395 +v -36.361549 -39.467541 -43.334003 +v -28.569061 -49.416569 -34.047298 +v -19.678720 -56.809361 -23.452192 +v -10.032122 -61.361820 -11.955818 +v -7.506893 95.563828 -13.683261 +v -14.725306 91.011368 -26.840694 +v -21.377817 83.618576 -38.966637 +v -27.208824 73.669540 -49.595142 +v -31.994171 61.546619 -58.317703 +v -35.550037 47.715679 -64.799164 +v -37.739700 32.708229 -68.790428 +v -38.479034 17.101002 -70.138092 +v -37.739700 1.493776 -68.790428 +v -35.550037 -13.513676 -64.799164 +v -31.994171 -27.344616 -58.317703 +v -27.208824 -39.467541 -49.595142 +v -21.377817 -49.416569 -38.966637 +v -14.725306 -56.809361 -26.840694 +v -7.506893 -61.361820 -13.683261 +v -4.693178 95.563828 -14.884863 +v -9.206003 91.011368 -29.197723 +v -13.365032 83.618576 -42.388504 +v -17.010483 73.669540 -53.950359 +v -20.002197 61.546619 -63.438896 +v -22.225267 47.715679 -70.489532 +v -23.594194 32.708229 -74.831291 +v -24.056406 17.101002 -76.297287 +v -23.594194 1.493776 -74.831291 +v -22.225267 -13.513676 -70.489532 +v -20.002197 -27.344616 -63.438896 +v -17.010483 -39.467541 -53.950359 +v -13.365032 -49.416569 -42.388504 +v -9.206003 -56.809361 -29.197723 +v -4.693178 -61.361820 -14.884863 +v -1.699107 95.563828 -15.514448 +v -3.332918 91.011368 -30.432697 +v -4.838636 83.618576 -44.181408 +v -6.158438 73.669540 -56.232300 +v -7.241543 61.546619 -66.122169 +v -8.046389 47.715679 -73.471031 +v -8.541981 32.708229 -77.996429 +v -8.709301 17.101002 -79.524429 +v -8.541981 1.493776 -77.996429 +v -8.046389 -13.513676 -73.471031 +v -7.241543 -27.344616 -66.122169 +v -6.158438 -39.467541 -56.232300 +v -4.838636 -49.416569 -44.181408 +v -3.332918 -56.809361 -30.432697 +v -1.699107 -61.361820 -15.514448 +v 1.360259 95.563828 -15.547821 +v 2.668244 91.011368 -30.498161 +v 3.873703 83.618576 -44.276443 +v 4.930273 73.669540 -56.353264 +v 5.797400 61.546619 -66.264404 +v 6.441704 47.715679 -73.629074 +v 6.838501 32.708229 -78.164200 +v 6.972488 17.101002 -79.695488 +v 6.838501 1.493776 -78.164200 +v 6.441704 -13.513676 -73.629074 +v 5.797400 -27.344616 -66.264404 +v 4.930273 -39.467541 -56.353264 +v 3.873703 -49.416569 -44.276443 +v 2.668244 -56.809361 -30.498161 +v 1.360259 -61.361820 -15.547821 +v 4.367352 95.563828 -14.983700 +v 8.566874 91.011368 -29.391598 +v 12.437177 83.618576 -42.669964 +v 15.829519 73.669540 -54.308601 +v 18.613546 61.546619 -63.860134 +v 20.682255 47.715679 -70.957596 +v 21.956179 32.708229 -75.328171 +v 22.386330 17.101002 -76.803894 +v 21.956179 1.493776 -75.328171 +v 20.682255 -13.513676 -70.957596 +v 18.613546 -27.344616 -63.860134 +v 15.829519 -39.467541 -54.308601 +v 12.437177 -49.416569 -42.669964 +v 8.566874 -56.809361 -29.391598 +v 4.367352 -61.361820 -14.983700 +v 7.206608 95.563828 -13.843763 +v 14.136278 91.011368 -27.155533 +v 20.522694 83.618576 -39.423695 +v 26.120441 73.669540 -50.176895 +v 30.714386 61.546619 -59.001755 +v 34.127991 47.715679 -65.559265 +v 36.230095 32.708229 -69.597321 +v 36.939873 17.101002 -70.960770 +v 36.230095 1.493776 -69.597321 +v 34.127991 -13.513676 -65.559265 +v 30.714386 -27.344616 -59.001755 +v 26.120441 -39.467541 -50.176895 +v 20.522694 -49.416569 -39.423695 +v 14.136278 -56.809361 -27.155533 +v 7.206608 -61.361820 -13.843763 +v 9.768919 95.563828 -12.171820 +v 19.162437 91.011368 -23.875895 +v 27.819538 83.618576 -34.662399 +v 35.407578 73.669540 -44.116917 +v 41.634888 61.546619 -51.875973 +v 46.262207 47.715679 -57.641518 +v 49.111706 32.708229 -61.191887 +v 50.073845 17.101002 -62.390663 +v 49.111706 1.493776 -61.191887 +v 46.262207 -13.513676 -57.641518 +v 41.634888 -27.344616 -51.875973 +v 35.407578 -39.467541 -44.116917 +v 27.819538 -49.416569 -34.662399 +v 19.162437 -56.809361 -23.875895 +v 9.768919 -61.361820 -12.171820 +v 11.955815 95.563828 -10.032120 +v 23.452192 91.011368 -19.678720 +v 43.334007 73.669540 -36.361546 +v 56.618595 47.715679 -47.508636 +v 56.618595 -13.513676 -47.508636 +v 43.334007 -39.467541 -36.361546 +v 34.047287 -49.416569 -28.569046 +v 23.452192 -56.809361 -19.678720 +v 11.955815 -61.361820 -10.032120 +vn -0.4288 -0.8286 0.3598 +vn -0.6384 -0.5528 0.5357 +vn -0.7515 -0.1939 0.6306 +vn -0.7660 -0.0000 0.6428 +vn -0.7515 0.1939 0.6306 +vn -0.6384 0.5528 0.5357 +vn -0.1762 -0.9796 0.0967 +vn -0.3401 -0.9217 0.1866 +vn -0.4908 -0.8286 0.2692 +vn -0.6226 -0.7041 0.3416 +vn -0.7306 -0.5528 0.4008 +vn -0.8108 -0.3805 0.4448 +vn -0.8601 -0.1939 0.4719 +vn -0.8767 -0.0000 0.4810 +vn -0.8601 0.1939 0.4719 +vn -0.8108 0.3805 0.4448 +vn -0.7306 0.5528 0.4008 +vn -0.6226 0.7041 0.3416 +vn -0.4908 0.8286 0.2692 +vn -0.3401 0.9217 0.1866 +vn -0.1762 0.9796 0.0967 +vn -0.1917 -0.9796 0.0604 +vn -0.3699 -0.9217 0.1166 +vn -0.5339 -0.8286 0.1683 +vn -0.6773 -0.7041 0.2135 +vn -0.7948 -0.5528 0.2506 +vn -0.8820 -0.3805 0.2781 +vn -0.9356 -0.1939 0.2950 +vn -0.9537 -0.0000 0.3007 +vn -0.9356 0.1939 0.2950 +vn -0.8820 0.3805 0.2781 +vn -0.7948 0.5528 0.2506 +vn -0.6773 0.7041 0.2135 +vn -0.5339 0.8286 0.1683 +vn -0.3699 0.9217 0.1166 +vn -0.1917 0.9796 0.0604 +vn -0.1998 -0.9796 0.0219 +vn -0.3856 -0.9217 0.0422 +vn -0.5564 -0.8286 0.0609 +vn -0.7059 -0.7041 0.0773 +vn -0.8284 -0.5528 0.0907 +vn -0.9193 -0.3805 0.1007 +vn -0.9752 -0.1939 0.1068 +vn -0.9941 -0.0000 0.1089 +vn -0.9752 0.1939 0.1068 +vn -0.9193 0.3805 0.1007 +vn -0.8284 0.5528 0.0907 +vn -0.7059 0.7041 0.0773 +vn -0.5564 0.8286 0.0609 +vn -0.3856 0.9217 0.0422 +vn -0.1998 0.9796 0.0219 +vn -0.2002 -0.9796 -0.0175 +vn -0.3864 -0.9217 -0.0338 +vn -0.5576 -0.8286 -0.0488 +vn -0.7074 -0.7041 -0.0619 +vn -0.8302 -0.5528 -0.0726 +vn -0.9212 -0.3805 -0.0806 +vn -0.9773 -0.1939 -0.0855 +vn -0.9962 -0.0000 -0.0872 +vn -0.9773 0.1939 -0.0855 +vn -0.9212 0.3805 -0.0806 +vn -0.8302 0.5528 -0.0726 +vn -0.7074 0.7041 -0.0619 +vn -0.5576 0.8286 -0.0488 +vn -0.3864 0.9217 -0.0338 +vn -0.2002 0.9796 -0.0175 +vn -0.1929 -0.9796 -0.0562 +vn -0.3724 -0.9217 -0.1085 +vn -0.5374 -0.8286 -0.1566 +vn -0.6818 -0.7041 -0.1987 +vn -0.8000 -0.5528 -0.2332 +vn -0.8878 -0.3805 -0.2588 +vn -0.9418 -0.1939 -0.2745 +vn -0.9600 -0.0000 -0.2798 +vn -0.9418 0.1939 -0.2745 +vn -0.8878 0.3805 -0.2588 +vn -0.8000 0.5528 -0.2332 +vn -0.6818 0.7041 -0.1987 +vn -0.5374 0.8286 -0.1566 +vn -0.3724 0.9217 -0.1085 +vn -0.1929 0.9796 -0.0562 +vn -0.0000 -1.0000 -0.0000 +vn -0.1783 -0.9796 -0.0928 +vn -0.3441 -0.9217 -0.1791 +vn -0.4965 -0.8286 -0.2585 +vn -0.6299 -0.7041 -0.3279 +vn -0.7392 -0.5528 -0.3848 +vn -0.8203 -0.3805 -0.4270 +vn -0.8702 -0.1939 -0.4530 +vn -0.8870 -0.0000 -0.4617 +vn -0.8702 0.1939 -0.4530 +vn -0.8203 0.3805 -0.4270 +vn -0.7392 0.5528 -0.3848 +vn -0.6299 0.7041 -0.3279 +vn -0.4965 0.8286 -0.2585 +vn -0.3441 0.9217 -0.1791 +vn -0.1783 0.9796 -0.0928 +vn -0.1567 -0.9796 -0.1258 +vn -0.3025 -0.9217 -0.2428 +vn -0.4366 -0.8286 -0.3504 +vn -0.5538 -0.7041 -0.4445 +vn -0.6499 -0.5528 -0.5216 +vn -0.7212 -0.3805 -0.5788 +vn -0.7651 -0.1939 -0.6140 +vn -0.7799 -0.0000 -0.6259 +vn -0.7651 0.1939 -0.6140 +vn -0.7212 0.3805 -0.5788 +vn -0.6499 0.5528 -0.5216 +vn -0.5538 0.7041 -0.4445 +vn -0.4366 0.8286 -0.3504 +vn -0.3025 0.9217 -0.2428 +vn -0.1567 0.9796 -0.1258 +vn -0.1292 -0.9796 -0.1539 +vn -0.2493 -0.9217 -0.2971 +vn -0.3598 -0.8286 -0.4288 +vn -0.4565 -0.7041 -0.5440 +vn -0.5357 -0.5528 -0.6384 +vn -0.5944 -0.3805 -0.7084 +vn -0.6306 -0.1939 -0.7515 +vn -0.6428 -0.0000 -0.7660 +vn -0.6306 0.1939 -0.7515 +vn -0.5944 0.3805 -0.7084 +vn -0.5357 0.5528 -0.6384 +vn -0.4565 0.7041 -0.5440 +vn -0.3598 0.8286 -0.4288 +vn -0.2493 0.9217 -0.2971 +vn -0.1292 0.9796 -0.1539 +vn -0.0967 -0.9796 -0.1762 +vn -0.1866 -0.9217 -0.3401 +vn -0.2692 -0.8286 -0.4908 +vn -0.3416 -0.7041 -0.6226 +vn -0.4008 -0.5528 -0.7306 +vn -0.4448 -0.3805 -0.8108 +vn -0.4719 -0.1939 -0.8601 +vn -0.4810 -0.0000 -0.8767 +vn -0.4719 0.1939 -0.8601 +vn -0.4448 0.3805 -0.8108 +vn -0.4008 0.5528 -0.7306 +vn -0.3416 0.7041 -0.6226 +vn -0.2692 0.8286 -0.4908 +vn -0.1866 0.9217 -0.3401 +vn -0.0967 0.9796 -0.1762 +vn -0.0604 -0.9796 -0.1917 +vn -0.1166 -0.9217 -0.3699 +vn -0.1683 -0.8286 -0.5339 +vn -0.2135 -0.7041 -0.6773 +vn -0.2506 -0.5528 -0.7948 +vn -0.2781 -0.3805 -0.8820 +vn -0.2950 -0.1939 -0.9356 +vn -0.3007 -0.0000 -0.9537 +vn -0.2950 0.1939 -0.9356 +vn -0.2781 0.3805 -0.8820 +vn -0.2506 0.5528 -0.7948 +vn -0.2135 0.7041 -0.6773 +vn -0.1683 0.8286 -0.5339 +vn -0.1166 0.9217 -0.3699 +vn -0.0604 0.9796 -0.1917 +vn -0.0219 -0.9796 -0.1998 +vn -0.0422 -0.9217 -0.3856 +vn -0.0609 -0.8286 -0.5564 +vn -0.0773 -0.7041 -0.7059 +vn -0.0907 -0.5528 -0.8284 +vn -0.1007 -0.3805 -0.9193 +vn -0.1068 -0.1939 -0.9752 +vn -0.1089 -0.0000 -0.9941 +vn -0.1068 0.1939 -0.9752 +vn -0.1007 0.3805 -0.9193 +vn -0.0907 0.5528 -0.8284 +vn -0.0773 0.7041 -0.7059 +vn -0.0609 0.8286 -0.5564 +vn -0.0422 0.9217 -0.3856 +vn -0.0219 0.9796 -0.1998 +vn 0.0175 -0.9796 -0.2002 +vn 0.0338 -0.9217 -0.3864 +vn 0.0488 -0.8286 -0.5576 +vn 0.0619 -0.7041 -0.7074 +vn 0.0726 -0.5528 -0.8302 +vn 0.0806 -0.3805 -0.9212 +vn 0.0855 -0.1939 -0.9773 +vn 0.0872 -0.0000 -0.9962 +vn 0.0855 0.1939 -0.9773 +vn 0.0806 0.3805 -0.9212 +vn 0.0726 0.5528 -0.8302 +vn 0.0619 0.7041 -0.7074 +vn 0.0488 0.8286 -0.5576 +vn 0.0338 0.9217 -0.3864 +vn 0.0175 0.9796 -0.2002 +vn 0.0562 -0.9796 -0.1929 +vn 0.1085 -0.9217 -0.3724 +vn 0.1566 -0.8286 -0.5374 +vn 0.1987 -0.7041 -0.6818 +vn 0.2332 -0.5528 -0.8000 +vn 0.2588 -0.3805 -0.8878 +vn 0.2745 -0.1939 -0.9418 +vn 0.2798 -0.0000 -0.9600 +vn 0.2745 0.1939 -0.9418 +vn 0.2588 0.3805 -0.8878 +vn 0.2332 0.5528 -0.8000 +vn 0.1987 0.7041 -0.6818 +vn 0.1566 0.8286 -0.5374 +vn 0.1085 0.9217 -0.3724 +vn 0.0562 0.9796 -0.1929 +vn 0.0928 -0.9796 -0.1783 +vn 0.1791 -0.9217 -0.3441 +vn 0.2585 -0.8286 -0.4965 +vn 0.3279 -0.7041 -0.6299 +vn 0.3848 -0.5528 -0.7392 +vn 0.4270 -0.3805 -0.8203 +vn 0.4530 -0.1939 -0.8702 +vn 0.4617 -0.0000 -0.8870 +vn 0.4530 0.1939 -0.8702 +vn 0.4270 0.3805 -0.8203 +vn 0.3848 0.5528 -0.7392 +vn 0.3279 0.7041 -0.6299 +vn 0.2585 0.8286 -0.4965 +vn 0.1791 0.9217 -0.3441 +vn 0.0928 0.9796 -0.1783 +vn 0.1258 -0.9796 -0.1567 +vn 0.2428 -0.9217 -0.3025 +vn 0.3504 -0.8286 -0.4366 +vn 0.4445 -0.7041 -0.5538 +vn 0.5216 -0.5528 -0.6499 +vn 0.5788 -0.3805 -0.7212 +vn 0.6140 -0.1939 -0.7651 +vn 0.6259 -0.0000 -0.7799 +vn 0.6140 0.1939 -0.7651 +vn 0.5788 0.3805 -0.7212 +vn 0.5216 0.5528 -0.6499 +vn 0.4445 0.7041 -0.5538 +vn 0.3504 0.8286 -0.4366 +vn 0.2428 0.9217 -0.3025 +vn 0.1258 0.9796 -0.1567 +vn 0.1539 -0.9796 -0.1292 +vn 0.2971 -0.9217 -0.2493 +vn 0.4288 -0.8286 -0.3598 +vn 0.5440 -0.7041 -0.4565 +vn 0.6384 -0.5528 -0.5357 +vn 0.7084 -0.3805 -0.5944 +vn 0.7515 -0.1939 -0.6306 +vn 0.7660 -0.0000 -0.6428 +vn 0.7515 0.1939 -0.6306 +vn 0.7084 0.3805 -0.5944 +vn 0.6384 0.5528 -0.5357 +vn 0.5440 0.7041 -0.4565 +vn 0.4288 0.8286 -0.3598 +vn 0.2971 0.9217 -0.2493 +vn 0.1539 0.9796 -0.1292 +vn 0.1762 -0.9796 -0.0967 +vn 0.3401 -0.9217 -0.1866 +vn 0.4908 -0.8286 -0.2692 +vn 0.6226 -0.7041 -0.3416 +vn 0.7306 -0.5528 -0.4008 +vn 0.8108 -0.3805 -0.4448 +vn 0.8601 -0.1939 -0.4719 +vn 0.8767 -0.0000 -0.4810 +vn 0.8601 0.1939 -0.4719 +vn 0.8108 0.3805 -0.4448 +vn 0.7306 0.5528 -0.4008 +vn 0.6226 0.7041 -0.3416 +vn 0.4908 0.8286 -0.2692 +vn 0.3401 0.9217 -0.1866 +vn 0.1762 0.9796 -0.0967 +vn 0.1917 -0.9796 -0.0604 +vn 0.3699 -0.9217 -0.1166 +vn 0.5339 -0.8286 -0.1683 +vn 0.6773 -0.7041 -0.2135 +vn 0.7948 -0.5528 -0.2506 +vn 0.8820 -0.3805 -0.2781 +vn 0.9356 -0.1939 -0.2950 +vn 0.9537 -0.0000 -0.3007 +vn 0.9356 0.1939 -0.2950 +vn 0.8820 0.3805 -0.2781 +vn 0.7948 0.5528 -0.2506 +vn 0.6773 0.7041 -0.2135 +vn 0.5339 0.8286 -0.1683 +vn 0.3699 0.9217 -0.1166 +vn 0.1917 0.9796 -0.0604 +vn 0.1998 -0.9796 -0.0219 +vn 0.3856 -0.9217 -0.0422 +vn 0.5564 -0.8286 -0.0609 +vn 0.7059 -0.7041 -0.0773 +vn 0.8284 -0.5528 -0.0907 +vn 0.9193 -0.3805 -0.1007 +vn 0.9752 -0.1939 -0.1068 +vn 0.9941 -0.0000 -0.1089 +vn 0.9752 0.1939 -0.1068 +vn 0.9193 0.3805 -0.1007 +vn 0.8284 0.5528 -0.0907 +vn 0.7059 0.7041 -0.0773 +vn 0.5564 0.8286 -0.0609 +vn 0.3856 0.9217 -0.0422 +vn 0.1998 0.9796 -0.0219 +vn 0.2002 -0.9796 0.0175 +vn 0.3864 -0.9217 0.0338 +vn 0.5576 -0.8286 0.0488 +vn 0.7074 -0.7041 0.0619 +vn 0.8302 -0.5528 0.0726 +vn 0.9212 -0.3805 0.0806 +vn 0.9773 -0.1939 0.0855 +vn 0.9962 -0.0000 0.0872 +vn 0.9773 0.1939 0.0855 +vn 0.9212 0.3805 0.0806 +vn 0.8302 0.5528 0.0726 +vn 0.7074 0.7041 0.0619 +vn 0.5576 0.8286 0.0488 +vn 0.3864 0.9217 0.0338 +vn 0.2002 0.9796 0.0175 +vn -0.0000 1.0000 -0.0000 +vn 0.1929 -0.9796 0.0562 +vn 0.3724 -0.9217 0.1085 +vn 0.5374 -0.8286 0.1566 +vn 0.6818 -0.7041 0.1987 +vn 0.8000 -0.5528 0.2332 +vn 0.8878 -0.3805 0.2588 +vn 0.9418 -0.1939 0.2745 +vn 0.9600 -0.0000 0.2798 +vn 0.9418 0.1939 0.2745 +vn 0.8878 0.3805 0.2588 +vn 0.8000 0.5528 0.2332 +vn 0.6818 0.7041 0.1987 +vn 0.5374 0.8286 0.1566 +vn 0.3724 0.9217 0.1085 +vn 0.1929 0.9796 0.0562 +vn 0.1783 -0.9796 0.0928 +vn 0.3441 -0.9217 0.1791 +vn 0.4965 -0.8286 0.2585 +vn 0.6299 -0.7041 0.3279 +vn 0.7392 -0.5528 0.3848 +vn 0.8203 -0.3805 0.4270 +vn 0.8702 -0.1939 0.4530 +vn 0.8870 -0.0000 0.4617 +vn 0.8702 0.1939 0.4530 +vn 0.8203 0.3805 0.4270 +vn 0.7392 0.5528 0.3848 +vn 0.6299 0.7041 0.3279 +vn 0.4965 0.8286 0.2585 +vn 0.3441 0.9217 0.1791 +vn 0.1783 0.9796 0.0928 +vn 0.1567 -0.9796 0.1258 +vn 0.3025 -0.9217 0.2428 +vn 0.4366 -0.8286 0.3504 +vn 0.5538 -0.7041 0.4445 +vn 0.6499 -0.5528 0.5216 +vn 0.7212 -0.3805 0.5788 +vn 0.7651 -0.1939 0.6140 +vn 0.7799 -0.0000 0.6259 +vn 0.7651 0.1939 0.6140 +vn 0.7212 0.3805 0.5788 +vn 0.6499 0.5528 0.5216 +vn 0.5538 0.7041 0.4445 +vn 0.4366 0.8286 0.3504 +vn 0.3025 0.9217 0.2428 +vn 0.1567 0.9796 0.1258 +vn 0.1292 -0.9796 0.1539 +vn 0.2493 -0.9217 0.2971 +vn 0.3598 -0.8286 0.4288 +vn 0.4565 -0.7041 0.5440 +vn 0.5357 -0.5528 0.6384 +vn 0.5944 -0.3805 0.7084 +vn 0.6306 -0.1939 0.7515 +vn 0.6428 -0.0000 0.7660 +vn 0.6306 0.1939 0.7515 +vn 0.5944 0.3805 0.7084 +vn 0.5357 0.5528 0.6384 +vn 0.4565 0.7041 0.5440 +vn 0.3598 0.8286 0.4288 +vn 0.2493 0.9217 0.2971 +vn 0.1292 0.9796 0.1539 +vn 0.0967 -0.9796 0.1762 +vn 0.1866 -0.9217 0.3401 +vn 0.2692 -0.8286 0.4908 +vn 0.3416 -0.7041 0.6226 +vn 0.4008 -0.5528 0.7306 +vn 0.4448 -0.3805 0.8108 +vn 0.4719 -0.1939 0.8601 +vn 0.4810 -0.0000 0.8767 +vn 0.4719 0.1939 0.8601 +vn 0.4448 0.3805 0.8108 +vn 0.4008 0.5528 0.7306 +vn 0.3416 0.7041 0.6226 +vn 0.2692 0.8286 0.4908 +vn 0.1866 0.9217 0.3401 +vn 0.0967 0.9796 0.1762 +vn 0.0604 -0.9796 0.1917 +vn 0.1166 -0.9217 0.3699 +vn 0.1683 -0.8286 0.5339 +vn 0.2135 -0.7041 0.6773 +vn 0.2506 -0.5528 0.7948 +vn 0.2781 -0.3805 0.8820 +vn 0.2950 -0.1939 0.9356 +vn 0.3007 -0.0000 0.9537 +vn 0.2950 0.1939 0.9356 +vn 0.2781 0.3805 0.8820 +vn 0.2506 0.5528 0.7948 +vn 0.2135 0.7041 0.6773 +vn 0.1683 0.8286 0.5339 +vn 0.1166 0.9217 0.3699 +vn 0.0604 0.9796 0.1917 +vn 0.0219 -0.9796 0.1998 +vn 0.0422 -0.9217 0.3856 +vn 0.0609 -0.8286 0.5564 +vn 0.0773 -0.7041 0.7059 +vn 0.0907 -0.5528 0.8284 +vn 0.1007 -0.3805 0.9193 +vn 0.1068 -0.1939 0.9752 +vn 0.1089 -0.0000 0.9941 +vn 0.1068 0.1939 0.9752 +vn 0.1007 0.3805 0.9193 +vn 0.0907 0.5528 0.8284 +vn 0.0773 0.7041 0.7059 +vn 0.0609 0.8286 0.5564 +vn 0.0422 0.9217 0.3856 +vn 0.0219 0.9796 0.1998 +vn -0.0175 -0.9796 0.2002 +vn -0.0338 -0.9217 0.3864 +vn -0.0488 -0.8286 0.5576 +vn -0.0619 -0.7041 0.7074 +vn -0.0726 -0.5528 0.8302 +vn -0.0806 -0.3805 0.9212 +vn -0.0855 -0.1939 0.9773 +vn -0.0872 -0.0000 0.9962 +vn -0.0855 0.1939 0.9773 +vn -0.0806 0.3805 0.9212 +vn -0.0726 0.5528 0.8302 +vn -0.0619 0.7041 0.7074 +vn -0.0488 0.8286 0.5576 +vn -0.0338 0.9217 0.3864 +vn -0.0175 0.9796 0.2002 +vn -0.0562 -0.9796 0.1929 +vn -0.1085 -0.9217 0.3724 +vn -0.1566 -0.8286 0.5374 +vn -0.1987 -0.7041 0.6818 +vn -0.2332 -0.5528 0.8000 +vn -0.2588 -0.3805 0.8878 +vn -0.2745 -0.1939 0.9418 +vn -0.2798 -0.0000 0.9600 +vn -0.2745 0.1939 0.9418 +vn -0.2588 0.3805 0.8878 +vn -0.2332 0.5528 0.8000 +vn -0.1987 0.7041 0.6818 +vn -0.1566 0.8286 0.5374 +vn -0.1085 0.9217 0.3724 +vn -0.0562 0.9796 0.1929 +vn -0.0928 -0.9796 0.1783 +vn -0.1791 -0.9217 0.3441 +vn -0.2585 -0.8286 0.4965 +vn -0.3279 -0.7041 0.6299 +vn -0.3848 -0.5528 0.7392 +vn -0.4270 -0.3805 0.8203 +vn -0.4530 -0.1939 0.8702 +vn -0.4617 -0.0000 0.8870 +vn -0.4530 0.1939 0.8702 +vn -0.4270 0.3805 0.8203 +vn -0.3848 0.5528 0.7392 +vn -0.3279 0.7041 0.6299 +vn -0.2585 0.8286 0.4965 +vn -0.1791 0.9217 0.3441 +vn -0.0928 0.9796 0.1783 +vn -0.1258 -0.9796 0.1567 +vn -0.2428 -0.9217 0.3025 +vn -0.3504 -0.8286 0.4366 +vn -0.4445 -0.7041 0.5538 +vn -0.5216 -0.5528 0.6499 +vn -0.5788 -0.3805 0.7212 +vn -0.6140 -0.1939 0.7651 +vn -0.6259 -0.0000 0.7799 +vn -0.6140 0.1939 0.7651 +vn -0.5788 0.3805 0.7212 +vn -0.5216 0.5528 0.6499 +vn -0.4445 0.7041 0.5538 +vn -0.3504 0.8286 0.4366 +vn -0.2428 0.9217 0.3025 +vn -0.1258 0.9796 0.1567 +vn -0.1539 -0.9796 0.1292 +vn -0.2971 -0.9217 0.2493 +vn -0.5440 -0.7041 0.4565 +vn -0.7084 -0.3805 0.5944 +vn -0.7084 0.3805 0.5944 +vn -0.5440 0.7041 0.4565 +vn -0.4288 0.8286 0.3598 +vn -0.2971 0.9217 0.2493 +vn -0.1539 0.9796 0.1292 +vt 0.750000 0.812500 +vt 0.718750 0.812500 +vt 0.718750 0.875000 +vt 0.750000 0.875000 +vt 0.750000 0.312500 +vt 0.718750 0.312500 +vt 0.718750 0.375000 +vt 0.750000 0.375000 +vt 0.750000 0.750000 +vt 0.718750 0.750000 +vt 0.750000 0.250000 +vt 0.718750 0.250000 +vt 0.750000 0.687500 +vt 0.718750 0.687500 +vt 0.750000 0.187500 +vt 0.718750 0.187500 +vt 0.750000 0.625000 +vt 0.718750 0.625000 +vt 0.750000 0.125000 +vt 0.718750 0.125000 +vt 0.750000 0.562500 +vt 0.718750 0.562500 +vt 0.750000 0.062500 +vt 0.718750 0.062500 +vt 0.750000 0.500000 +vt 0.718750 0.500000 +vt 0.750000 0.937500 +vt 0.718750 0.937500 +vt 0.734375 1.000000 +vt 0.734375 0.000000 +vt 0.750000 0.437500 +vt 0.718750 0.437500 +vt 0.687500 0.875000 +vt 0.687500 0.937500 +vt 0.687500 0.375000 +vt 0.687500 0.437500 +vt 0.687500 0.812500 +vt 0.687500 0.312500 +vt 0.687500 0.750000 +vt 0.687500 0.250000 +vt 0.687500 0.687500 +vt 0.687500 0.187500 +vt 0.687500 0.625000 +vt 0.687500 0.125000 +vt 0.687500 0.562500 +vt 0.687500 0.062500 +vt 0.687500 0.500000 +vt 0.703125 1.000000 +vt 0.703125 0.000000 +vt 0.656250 0.625000 +vt 0.656250 0.687500 +vt 0.656250 0.125000 +vt 0.656250 0.187500 +vt 0.656250 0.562500 +vt 0.656250 0.062500 +vt 0.656250 0.500000 +vt 0.656250 0.937500 +vt 0.671875 1.000000 +vt 0.671875 0.000000 +vt 0.656250 0.437500 +vt 0.656250 0.875000 +vt 0.656250 0.375000 +vt 0.656250 0.812500 +vt 0.656250 0.312500 +vt 0.656250 0.750000 +vt 0.656250 0.250000 +vt 0.625000 0.375000 +vt 0.625000 0.437500 +vt 0.625000 0.812500 +vt 0.625000 0.875000 +vt 0.625000 0.312500 +vt 0.625000 0.750000 +vt 0.625000 0.250000 +vt 0.625000 0.687500 +vt 0.625000 0.187500 +vt 0.625000 0.625000 +vt 0.625000 0.125000 +vt 0.625000 0.562500 +vt 0.625000 0.062500 +vt 0.625000 0.500000 +vt 0.625000 0.937500 +vt 0.640625 1.000000 +vt 0.640625 0.000000 +vt 0.593750 0.125000 +vt 0.593750 0.187500 +vt 0.593750 0.562500 +vt 0.593750 0.625000 +vt 0.593750 0.062500 +vt 0.593750 0.500000 +vt 0.593750 0.937500 +vt 0.609375 1.000000 +vt 0.609375 0.000000 +vt 0.593750 0.437500 +vt 0.593750 0.875000 +vt 0.593750 0.375000 +vt 0.593750 0.812500 +vt 0.593750 0.312500 +vt 0.593750 0.750000 +vt 0.593750 0.250000 +vt 0.593750 0.687500 +vt 0.562500 0.812500 +vt 0.562500 0.875000 +vt 0.562500 0.312500 +vt 0.562500 0.375000 +vt 0.562500 0.750000 +vt 0.562500 0.250000 +vt 0.562500 0.687500 +vt 0.562500 0.187500 +vt 0.562500 0.625000 +vt 0.562500 0.125000 +vt 0.562500 0.562500 +vt 0.562500 0.062500 +vt 0.562500 0.500000 +vt 0.562500 0.937500 +vt 0.578125 1.000000 +vt 0.578125 0.000000 +vt 0.562500 0.437500 +vt 0.531250 0.562500 +vt 0.531250 0.625000 +vt 0.531250 0.062500 +vt 0.531250 0.125000 +vt 0.531250 0.500000 +vt 0.531250 0.937500 +vt 0.546875 1.000000 +vt 0.546875 0.000000 +vt 0.531250 0.437500 +vt 0.531250 0.875000 +vt 0.531250 0.375000 +vt 0.531250 0.812500 +vt 0.531250 0.312500 +vt 0.531250 0.750000 +vt 0.531250 0.250000 +vt 0.531250 0.687500 +vt 0.531250 0.187500 +vt 0.500000 0.312500 +vt 0.500000 0.375000 +vt 0.500000 0.750000 +vt 0.500000 0.812500 +vt 0.500000 0.250000 +vt 0.500000 0.687500 +vt 0.500000 0.187500 +vt 0.500000 0.625000 +vt 0.500000 0.125000 +vt 0.500000 0.562500 +vt 0.500000 0.062500 +vt 0.500000 0.500000 +vt 0.500000 0.937500 +vt 0.515625 1.000000 +vt 0.515625 0.000000 +vt 0.500000 0.437500 +vt 0.500000 0.875000 +vt 0.468750 0.062500 +vt 0.468750 0.125000 +vt 0.468750 0.500000 +vt 0.468750 0.562500 +vt 0.468750 0.937500 +vt 0.484375 1.000000 +vt 0.484375 0.000000 +vt 0.468750 0.437500 +vt 0.468750 0.875000 +vt 0.468750 0.375000 +vt 0.468750 0.812500 +vt 0.468750 0.312500 +vt 0.468750 0.750000 +vt 0.468750 0.250000 +vt 0.468750 0.687500 +vt 0.468750 0.187500 +vt 0.468750 0.625000 +vt 0.437500 0.250000 +vt 0.437500 0.312500 +vt 0.437500 0.687500 +vt 0.437500 0.750000 +vt 0.437500 0.187500 +vt 0.437500 0.625000 +vt 0.437500 0.125000 +vt 0.437500 0.562500 +vt 0.437500 0.062500 +vt 0.437500 0.500000 +vt 0.437500 0.937500 +vt 0.453125 1.000000 +vt 0.453125 0.000000 +vt 0.437500 0.437500 +vt 0.437500 0.875000 +vt 0.437500 0.375000 +vt 0.437500 0.812500 +vt 0.406250 0.937500 +vt 0.421875 1.000000 +vt 0.421875 0.000000 +vt 0.406250 0.062500 +vt 0.406250 0.437500 +vt 0.406250 0.500000 +vt 0.406250 0.875000 +vt 0.406250 0.375000 +vt 0.406250 0.812500 +vt 0.406250 0.312500 +vt 0.406250 0.750000 +vt 0.406250 0.250000 +vt 0.406250 0.687500 +vt 0.406250 0.187500 +vt 0.406250 0.625000 +vt 0.406250 0.125000 +vt 0.406250 0.562500 +vt 0.375000 0.687500 +vt 0.375000 0.750000 +vt 0.375000 0.187500 +vt 0.375000 0.250000 +vt 0.375000 0.625000 +vt 0.375000 0.125000 +vt 0.375000 0.562500 +vt 0.375000 0.062500 +vt 0.375000 0.500000 +vt 0.375000 0.937500 +vt 0.390625 1.000000 +vt 0.390625 0.000000 +vt 0.375000 0.437500 +vt 0.375000 0.875000 +vt 0.375000 0.375000 +vt 0.375000 0.812500 +vt 0.375000 0.312500 +vt 0.343750 0.437500 +vt 0.343750 0.500000 +vt 0.343750 0.875000 +vt 0.343750 0.937500 +vt 0.343750 0.375000 +vt 0.343750 0.812500 +vt 0.343750 0.312500 +vt 0.343750 0.750000 +vt 0.343750 0.250000 +vt 0.343750 0.687500 +vt 0.343750 0.187500 +vt 0.343750 0.625000 +vt 0.343750 0.125000 +vt 0.343750 0.562500 +vt 0.343750 0.062500 +vt 0.359375 1.000000 +vt 0.359375 0.000000 +vt 0.312500 0.187500 +vt 0.312500 0.250000 +vt 0.312500 0.625000 +vt 0.312500 0.687500 +vt 0.312500 0.125000 +vt 0.312500 0.562500 +vt 0.312500 0.062500 +vt 0.312500 0.500000 +vt 0.312500 0.937500 +vt 0.328125 1.000000 +vt 0.328125 0.000000 +vt 0.312500 0.437500 +vt 0.312500 0.875000 +vt 0.312500 0.375000 +vt 0.312500 0.812500 +vt 0.312500 0.312500 +vt 0.312500 0.750000 +vt 0.281250 0.875000 +vt 0.281250 0.937500 +vt 0.281250 0.375000 +vt 0.281250 0.437500 +vt 0.281250 0.812500 +vt 0.281250 0.312500 +vt 0.281250 0.750000 +vt 0.281250 0.250000 +vt 0.281250 0.687500 +vt 0.281250 0.187500 +vt 0.281250 0.625000 +vt 0.281250 0.125000 +vt 0.281250 0.562500 +vt 0.281250 0.062500 +vt 0.281250 0.500000 +vt 0.296875 1.000000 +vt 0.296875 0.000000 +vt 0.250000 0.625000 +vt 0.250000 0.687500 +vt 0.250000 0.125000 +vt 0.250000 0.187500 +vt 0.250000 0.562500 +vt 0.250000 0.062500 +vt 0.250000 0.500000 +vt 0.250000 0.937500 +vt 0.265625 1.000000 +vt 0.265625 0.000000 +vt 0.250000 0.437500 +vt 0.250000 0.875000 +vt 0.250000 0.375000 +vt 0.250000 0.812500 +vt 0.250000 0.312500 +vt 0.250000 0.750000 +vt 0.250000 0.250000 +vt 0.218750 0.375000 +vt 0.218750 0.437500 +vt 0.218750 0.812500 +vt 0.218750 0.875000 +vt 0.218750 0.312500 +vt 0.218750 0.750000 +vt 0.218750 0.250000 +vt 0.218750 0.687500 +vt 0.218750 0.187500 +vt 0.218750 0.625000 +vt 0.218750 0.125000 +vt 0.218750 0.562500 +vt 0.218750 0.062500 +vt 0.218750 0.500000 +vt 0.218750 0.937500 +vt 0.234375 1.000000 +vt 0.234375 0.000000 +vt 0.187500 0.125000 +vt 0.187500 0.187500 +vt 0.187500 0.562500 +vt 0.187500 0.625000 +vt 0.187500 0.062500 +vt 0.187500 0.500000 +vt 0.187500 0.937500 +vt 0.203125 1.000000 +vt 0.203125 0.000000 +vt 0.187500 0.437500 +vt 0.187500 0.875000 +vt 0.187500 0.375000 +vt 0.187500 0.812500 +vt 0.187500 0.312500 +vt 0.187500 0.750000 +vt 0.187500 0.250000 +vt 0.187500 0.687500 +vt 0.156250 0.312500 +vt 0.156250 0.375000 +vt 0.156250 0.750000 +vt 0.156250 0.812500 +vt 0.156250 0.250000 +vt 0.156250 0.687500 +vt 0.156250 0.187500 +vt 0.156250 0.625000 +vt 0.156250 0.125000 +vt 0.156250 0.562500 +vt 0.156250 0.062500 +vt 0.156250 0.500000 +vt 0.156250 0.937500 +vt 0.171875 1.000000 +vt 0.171875 0.000000 +vt 0.156250 0.437500 +vt 0.156250 0.875000 +vt 0.125000 0.062500 +vt 0.125000 0.125000 +vt 0.125000 0.500000 +vt 0.125000 0.562500 +vt 0.125000 0.937500 +vt 0.140625 1.000000 +vt 0.140625 0.000000 +vt 0.125000 0.437500 +vt 0.125000 0.875000 +vt 0.125000 0.375000 +vt 0.125000 0.812500 +vt 0.125000 0.312500 +vt 0.125000 0.750000 +vt 0.125000 0.250000 +vt 0.125000 0.687500 +vt 0.125000 0.187500 +vt 0.125000 0.625000 +vt 0.093750 0.750000 +vt 0.093750 0.812500 +vt 0.093750 0.250000 +vt 0.093750 0.312500 +vt 0.093750 0.687500 +vt 0.093750 0.187500 +vt 0.093750 0.625000 +vt 0.093750 0.125000 +vt 0.093750 0.562500 +vt 0.093750 0.062500 +vt 0.093750 0.500000 +vt 0.093750 0.937500 +vt 0.109375 1.000000 +vt 0.109375 0.000000 +vt 0.093750 0.437500 +vt 0.093750 0.875000 +vt 0.093750 0.375000 +vt 0.062500 0.500000 +vt 0.062500 0.562500 +vt 0.062500 0.937500 +vt 0.078125 1.000000 +vt 0.078125 0.000000 +vt 0.062500 0.062500 +vt 0.062500 0.437500 +vt 0.062500 0.875000 +vt 0.062500 0.375000 +vt 0.062500 0.812500 +vt 0.062500 0.312500 +vt 0.062500 0.750000 +vt 0.062500 0.250000 +vt 0.062500 0.687500 +vt 0.062500 0.187500 +vt 0.062500 0.625000 +vt 0.062500 0.125000 +vt 0.031250 0.250000 +vt 0.031250 0.312500 +vt 0.031250 0.687500 +vt 0.031250 0.750000 +vt 0.031250 0.187500 +vt 0.031250 0.625000 +vt 0.031250 0.125000 +vt 0.031250 0.562500 +vt 0.031250 0.062500 +vt 0.031250 0.500000 +vt 0.031250 0.937500 +vt 0.046875 1.000000 +vt 0.046875 0.000000 +vt 0.031250 0.437500 +vt 0.031250 0.875000 +vt 0.031250 0.375000 +vt 0.031250 0.812500 +vt 0.000000 0.937500 +vt 0.015625 1.000000 +vt 0.015625 0.000000 +vt 0.000000 0.062500 +vt 0.000000 0.437500 +vt 0.000000 0.500000 +vt 0.000000 0.875000 +vt 0.000000 0.375000 +vt 0.000000 0.812500 +vt 0.000000 0.312500 +vt 0.000000 0.750000 +vt 0.000000 0.250000 +vt 0.000000 0.687500 +vt 0.000000 0.187500 +vt 0.000000 0.625000 +vt 0.000000 0.125000 +vt 0.000000 0.562500 +vt 1.000000 0.687500 +vt 0.968750 0.687500 +vt 0.968750 0.750000 +vt 1.000000 0.750000 +vt 1.000000 0.187500 +vt 0.968750 0.187500 +vt 0.968750 0.250000 +vt 1.000000 0.250000 +vt 1.000000 0.625000 +vt 0.968750 0.625000 +vt 1.000000 0.125000 +vt 0.968750 0.125000 +vt 1.000000 0.562500 +vt 0.968750 0.562500 +vt 1.000000 0.062500 +vt 0.968750 0.062500 +vt 1.000000 0.500000 +vt 0.968750 0.500000 +vt 1.000000 0.937500 +vt 0.968750 0.937500 +vt 0.984375 1.000000 +vt 0.984375 0.000000 +vt 1.000000 0.437500 +vt 0.968750 0.437500 +vt 1.000000 0.875000 +vt 0.968750 0.875000 +vt 1.000000 0.375000 +vt 0.968750 0.375000 +vt 1.000000 0.812500 +vt 0.968750 0.812500 +vt 1.000000 0.312500 +vt 0.968750 0.312500 +vt 0.937500 0.437500 +vt 0.937500 0.500000 +vt 0.937500 0.875000 +vt 0.937500 0.937500 +vt 0.937500 0.375000 +vt 0.937500 0.812500 +vt 0.937500 0.312500 +vt 0.937500 0.750000 +vt 0.937500 0.250000 +vt 0.937500 0.687500 +vt 0.937500 0.187500 +vt 0.937500 0.625000 +vt 0.937500 0.125000 +vt 0.937500 0.562500 +vt 0.937500 0.062500 +vt 0.953125 1.000000 +vt 0.953125 0.000000 +vt 0.906250 0.187500 +vt 0.906250 0.250000 +vt 0.906250 0.625000 +vt 0.906250 0.687500 +vt 0.906250 0.125000 +vt 0.906250 0.562500 +vt 0.906250 0.062500 +vt 0.906250 0.500000 +vt 0.906250 0.937500 +vt 0.921875 1.000000 +vt 0.921875 0.000000 +vt 0.906250 0.437500 +vt 0.906250 0.875000 +vt 0.906250 0.375000 +vt 0.906250 0.812500 +vt 0.906250 0.312500 +vt 0.906250 0.750000 +vt 0.875000 0.375000 +vt 0.875000 0.437500 +vt 0.875000 0.812500 +vt 0.875000 0.875000 +vt 0.875000 0.312500 +vt 0.875000 0.750000 +vt 0.875000 0.250000 +vt 0.875000 0.687500 +vt 0.875000 0.187500 +vt 0.875000 0.625000 +vt 0.875000 0.125000 +vt 0.875000 0.562500 +vt 0.875000 0.062500 +vt 0.875000 0.500000 +vt 0.875000 0.937500 +vt 0.890625 1.000000 +vt 0.890625 0.000000 +vt 0.843750 0.125000 +vt 0.843750 0.187500 +vt 0.843750 0.562500 +vt 0.843750 0.625000 +vt 0.843750 0.062500 +vt 0.843750 0.500000 +vt 0.843750 0.937500 +vt 0.859375 1.000000 +vt 0.859375 0.000000 +vt 0.843750 0.437500 +vt 0.843750 0.875000 +vt 0.843750 0.375000 +vt 0.843750 0.812500 +vt 0.843750 0.312500 +vt 0.843750 0.750000 +vt 0.843750 0.250000 +vt 0.843750 0.687500 +vt 0.812500 0.812500 +vt 0.812500 0.875000 +vt 0.812500 0.312500 +vt 0.812500 0.375000 +vt 0.812500 0.750000 +vt 0.812500 0.250000 +vt 0.812500 0.687500 +vt 0.812500 0.187500 +vt 0.812500 0.625000 +vt 0.812500 0.125000 +vt 0.812500 0.562500 +vt 0.812500 0.062500 +vt 0.812500 0.500000 +vt 0.812500 0.937500 +vt 0.828125 1.000000 +vt 0.828125 0.000000 +vt 0.812500 0.437500 +vt 0.781250 0.562500 +vt 0.781250 0.625000 +vt 0.781250 0.062500 +vt 0.781250 0.125000 +vt 0.781250 0.500000 +vt 0.781250 0.937500 +vt 0.796875 1.000000 +vt 0.796875 0.000000 +vt 0.781250 0.437500 +vt 0.781250 0.875000 +vt 0.781250 0.375000 +vt 0.781250 0.812500 +vt 0.781250 0.312500 +vt 0.781250 0.750000 +vt 0.781250 0.250000 +vt 0.781250 0.687500 +vt 0.781250 0.187500 +vt 0.765625 1.000000 +vt 0.765625 0.000000 +s 1 +f 1/1/1 9/2/9 8/3/8 475/4/475 +f 6/5/6 17/6/17 16/7/16 478/8/478 +f 476/9/476 10/10/10 9/2/9 1/1/1 +f 479/11/479 18/12/18 17/6/17 6/5/6 +f 2/13/2 11/14/11 10/10/10 476/9/476 +f 480/15/480 19/16/19 18/12/18 479/11/479 +f 477/17/477 12/18/12 11/14/11 2/13/2 +f 481/19/481 20/20/20 19/16/19 480/15/480 +f 3/21/3 13/22/13 12/18/12 477/17/477 +f 482/23/482 21/24/21 20/20/20 481/19/481 +f 4/25/4 14/26/14 13/22/13 3/21/3 +f 474/27/474 7/28/7 82/29/82 +f 308/30/308 21/24/21 482/23/482 +f 5/31/5 15/32/15 14/26/14 4/25/4 +f 475/4/475 8/3/8 7/28/7 474/27/474 +f 478/8/478 16/7/16 15/32/15 5/31/5 +f 8/3/8 23/33/23 22/34/22 7/28/7 +f 16/7/16 31/35/31 30/36/30 15/32/15 +f 9/2/9 24/37/24 23/33/23 8/3/8 +f 17/6/17 32/38/32 31/35/31 16/7/16 +f 10/10/10 25/39/25 24/37/24 9/2/9 +f 18/12/18 33/40/33 32/38/32 17/6/17 +f 11/14/11 26/41/26 25/39/25 10/10/10 +f 19/16/19 34/42/34 33/40/33 18/12/18 +f 12/18/12 27/43/27 26/41/26 11/14/11 +f 20/20/20 35/44/35 34/42/34 19/16/19 +f 13/22/13 28/45/28 27/43/27 12/18/12 +f 21/24/21 36/46/36 35/44/35 20/20/20 +f 14/26/14 29/47/29 28/45/28 13/22/13 +f 7/28/7 22/34/22 82/48/82 +f 308/49/308 36/46/36 21/24/21 +f 15/32/15 30/36/30 29/47/29 14/26/14 +f 27/43/27 42/50/42 41/51/41 26/41/26 +f 35/44/35 50/52/50 49/53/49 34/42/34 +f 28/45/28 43/54/43 42/50/42 27/43/27 +f 36/46/36 51/55/51 50/52/50 35/44/35 +f 29/47/29 44/56/44 43/54/43 28/45/28 +f 22/34/22 37/57/37 82/58/82 +f 308/59/308 51/55/51 36/46/36 +f 30/36/30 45/60/45 44/56/44 29/47/29 +f 23/33/23 38/61/38 37/57/37 22/34/22 +f 31/35/31 46/62/46 45/60/45 30/36/30 +f 24/37/24 39/63/39 38/61/38 23/33/23 +f 32/38/32 47/64/47 46/62/46 31/35/31 +f 25/39/25 40/65/40 39/63/39 24/37/24 +f 33/40/33 48/66/48 47/64/47 32/38/32 +f 26/41/26 41/51/41 40/65/40 25/39/25 +f 34/42/34 49/53/49 48/66/48 33/40/33 +f 46/62/46 61/67/61 60/68/60 45/60/45 +f 39/63/39 54/69/54 53/70/53 38/61/38 +f 47/64/47 62/71/62 61/67/61 46/62/46 +f 40/65/40 55/72/55 54/69/54 39/63/39 +f 48/66/48 63/73/63 62/71/62 47/64/47 +f 41/51/41 56/74/56 55/72/55 40/65/40 +f 49/53/49 64/75/64 63/73/63 48/66/48 +f 42/50/42 57/76/57 56/74/56 41/51/41 +f 50/52/50 65/77/65 64/75/64 49/53/49 +f 43/54/43 58/78/58 57/76/57 42/50/42 +f 51/55/51 66/79/66 65/77/65 50/52/50 +f 44/56/44 59/80/59 58/78/58 43/54/43 +f 37/57/37 52/81/52 82/82/82 +f 308/83/308 66/79/66 51/55/51 +f 45/60/45 60/68/60 59/80/59 44/56/44 +f 38/61/38 53/70/53 52/81/52 37/57/37 +f 65/77/65 80/84/80 79/85/79 64/75/64 +f 58/78/58 73/86/73 72/87/72 57/76/57 +f 66/79/66 81/88/81 80/84/80 65/77/65 +f 59/80/59 74/89/74 73/86/73 58/78/58 +f 52/81/52 67/90/67 82/91/82 +f 308/92/308 81/88/81 66/79/66 +f 60/68/60 75/93/75 74/89/74 59/80/59 +f 53/70/53 68/94/68 67/90/67 52/81/52 +f 61/67/61 76/95/76 75/93/75 60/68/60 +f 54/69/54 69/96/69 68/94/68 53/70/53 +f 62/71/62 77/97/77 76/95/76 61/67/61 +f 55/72/55 70/98/70 69/96/69 54/69/54 +f 63/73/63 78/99/78 77/97/77 62/71/62 +f 56/74/56 71/100/71 70/98/70 55/72/55 +f 64/75/64 79/85/79 78/99/78 63/73/63 +f 57/76/57 72/87/72 71/100/71 56/74/56 +f 69/96/69 85/101/85 84/102/84 68/94/68 +f 77/97/77 93/103/93 92/104/92 76/95/76 +f 70/98/70 86/105/86 85/101/85 69/96/69 +f 78/99/78 94/106/94 93/103/93 77/97/77 +f 71/100/71 87/107/87 86/105/86 70/98/70 +f 79/85/79 95/108/95 94/106/94 78/99/78 +f 72/87/72 88/109/88 87/107/87 71/100/71 +f 80/84/80 96/110/96 95/108/95 79/85/79 +f 73/86/73 89/111/89 88/109/88 72/87/72 +f 81/88/81 97/112/97 96/110/96 80/84/80 +f 74/89/74 90/113/90 89/111/89 73/86/73 +f 67/90/67 83/114/83 82/115/82 +f 308/116/308 97/112/97 81/88/81 +f 75/93/75 91/117/91 90/113/90 74/89/74 +f 68/94/68 84/102/84 83/114/83 67/90/67 +f 76/95/76 92/104/92 91/117/91 75/93/75 +f 89/111/89 104/118/104 103/119/103 88/109/88 +f 97/112/97 112/120/112 111/121/111 96/110/96 +f 90/113/90 105/122/105 104/118/104 89/111/89 +f 83/114/83 98/123/98 82/124/82 +f 308/125/308 112/120/112 97/112/97 +f 91/117/91 106/126/106 105/122/105 90/113/90 +f 84/102/84 99/127/99 98/123/98 83/114/83 +f 92/104/92 107/128/107 106/126/106 91/117/91 +f 85/101/85 100/129/100 99/127/99 84/102/84 +f 93/103/93 108/130/108 107/128/107 92/104/92 +f 86/105/86 101/131/101 100/129/100 85/101/85 +f 94/106/94 109/132/109 108/130/108 93/103/93 +f 87/107/87 102/133/102 101/131/101 86/105/86 +f 95/108/95 110/134/110 109/132/109 94/106/94 +f 88/109/88 103/119/103 102/133/102 87/107/87 +f 96/110/96 111/121/111 110/134/110 95/108/95 +f 108/130/108 123/135/123 122/136/122 107/128/107 +f 101/131/101 116/137/116 115/138/115 100/129/100 +f 109/132/109 124/139/124 123/135/123 108/130/108 +f 102/133/102 117/140/117 116/137/116 101/131/101 +f 110/134/110 125/141/125 124/139/124 109/132/109 +f 103/119/103 118/142/118 117/140/117 102/133/102 +f 111/121/111 126/143/126 125/141/125 110/134/110 +f 104/118/104 119/144/119 118/142/118 103/119/103 +f 112/120/112 127/145/127 126/143/126 111/121/111 +f 105/122/105 120/146/120 119/144/119 104/118/104 +f 98/123/98 113/147/113 82/148/82 +f 308/149/308 127/145/127 112/120/112 +f 106/126/106 121/150/121 120/146/120 105/122/105 +f 99/127/99 114/151/114 113/147/113 98/123/98 +f 107/128/107 122/136/122 121/150/121 106/126/106 +f 100/129/100 115/138/115 114/151/114 99/127/99 +f 127/145/127 142/152/142 141/153/141 126/143/126 +f 120/146/120 135/154/135 134/155/134 119/144/119 +f 113/147/113 128/156/128 82/157/82 +f 308/158/308 142/152/142 127/145/127 +f 121/150/121 136/159/136 135/154/135 120/146/120 +f 114/151/114 129/160/129 128/156/128 113/147/113 +f 122/136/122 137/161/137 136/159/136 121/150/121 +f 115/138/115 130/162/130 129/160/129 114/151/114 +f 123/135/123 138/163/138 137/161/137 122/136/122 +f 116/137/116 131/164/131 130/162/130 115/138/115 +f 124/139/124 139/165/139 138/163/138 123/135/123 +f 117/140/117 132/166/132 131/164/131 116/137/116 +f 125/141/125 140/167/140 139/165/139 124/139/124 +f 118/142/118 133/168/133 132/166/132 117/140/117 +f 126/143/126 141/153/141 140/167/140 125/141/125 +f 119/144/119 134/155/134 133/168/133 118/142/118 +f 139/165/139 154/169/154 153/170/153 138/163/138 +f 132/166/132 147/171/147 146/172/146 131/164/131 +f 140/167/140 155/173/155 154/169/154 139/165/139 +f 133/168/133 148/174/148 147/171/147 132/166/132 +f 141/153/141 156/175/156 155/173/155 140/167/140 +f 134/155/134 149/176/149 148/174/148 133/168/133 +f 142/152/142 157/177/157 156/175/156 141/153/141 +f 135/154/135 150/178/150 149/176/149 134/155/134 +f 128/156/128 143/179/143 82/180/82 +f 308/181/308 157/177/157 142/152/142 +f 136/159/136 151/182/151 150/178/150 135/154/135 +f 129/160/129 144/183/144 143/179/143 128/156/128 +f 137/161/137 152/184/152 151/182/151 136/159/136 +f 130/162/130 145/185/145 144/183/144 129/160/129 +f 138/163/138 153/170/153 152/184/152 137/161/137 +f 131/164/131 146/172/146 145/185/145 130/162/130 +f 143/179/143 158/186/158 82/187/82 +f 308/188/308 172/189/172 157/177/157 +f 151/182/151 166/190/166 165/191/165 150/178/150 +f 144/183/144 159/192/159 158/186/158 143/179/143 +f 152/184/152 167/193/167 166/190/166 151/182/151 +f 145/185/145 160/194/160 159/192/159 144/183/144 +f 153/170/153 168/195/168 167/193/167 152/184/152 +f 146/172/146 161/196/161 160/194/160 145/185/145 +f 154/169/154 169/197/169 168/195/168 153/170/153 +f 147/171/147 162/198/162 161/196/161 146/172/146 +f 155/173/155 170/199/170 169/197/169 154/169/154 +f 148/174/148 163/200/163 162/198/162 147/171/147 +f 156/175/156 171/201/171 170/199/170 155/173/155 +f 149/176/149 164/202/164 163/200/163 148/174/148 +f 157/177/157 172/189/172 171/201/171 156/175/156 +f 150/178/150 165/191/165 164/202/164 149/176/149 +f 162/198/162 177/203/177 176/204/176 161/196/161 +f 170/199/170 185/205/185 184/206/184 169/197/169 +f 163/200/163 178/207/178 177/203/177 162/198/162 +f 171/201/171 186/208/186 185/205/185 170/199/170 +f 164/202/164 179/209/179 178/207/178 163/200/163 +f 172/189/172 187/210/187 186/208/186 171/201/171 +f 165/191/165 180/211/180 179/209/179 164/202/164 +f 158/186/158 173/212/173 82/213/82 +f 308/214/308 187/210/187 172/189/172 +f 166/190/166 181/215/181 180/211/180 165/191/165 +f 159/192/159 174/216/174 173/212/173 158/186/158 +f 167/193/167 182/217/182 181/215/181 166/190/166 +f 160/194/160 175/218/175 174/216/174 159/192/159 +f 168/195/168 183/219/183 182/217/182 167/193/167 +f 161/196/161 176/204/176 175/218/175 160/194/160 +f 169/197/169 184/206/184 183/219/183 168/195/168 +f 181/215/181 196/220/196 195/221/195 180/211/180 +f 174/216/174 189/222/189 188/223/188 173/212/173 +f 182/217/182 197/224/197 196/220/196 181/215/181 +f 175/218/175 190/225/190 189/222/189 174/216/174 +f 183/219/183 198/226/198 197/224/197 182/217/182 +f 176/204/176 191/227/191 190/225/190 175/218/175 +f 184/206/184 199/228/199 198/226/198 183/219/183 +f 177/203/177 192/229/192 191/227/191 176/204/176 +f 185/205/185 200/230/200 199/228/199 184/206/184 +f 178/207/178 193/231/193 192/229/192 177/203/177 +f 186/208/186 201/232/201 200/230/200 185/205/185 +f 179/209/179 194/233/194 193/231/193 178/207/178 +f 187/210/187 202/234/202 201/232/201 186/208/186 +f 180/211/180 195/221/195 194/233/194 179/209/179 +f 173/212/173 188/223/188 82/235/82 +f 308/236/308 202/234/202 187/210/187 +f 200/230/200 215/237/215 214/238/214 199/228/199 +f 193/231/193 208/239/208 207/240/207 192/229/192 +f 201/232/201 216/241/216 215/237/215 200/230/200 +f 194/233/194 209/242/209 208/239/208 193/231/193 +f 202/234/202 217/243/217 216/241/216 201/232/201 +f 195/221/195 210/244/210 209/242/209 194/233/194 +f 188/223/188 203/245/203 82/246/82 +f 308/247/308 217/243/217 202/234/202 +f 196/220/196 211/248/211 210/244/210 195/221/195 +f 189/222/189 204/249/204 203/245/203 188/223/188 +f 197/224/197 212/250/212 211/248/211 196/220/196 +f 190/225/190 205/251/205 204/249/204 189/222/189 +f 198/226/198 213/252/213 212/250/212 197/224/197 +f 191/227/191 206/253/206 205/251/205 190/225/190 +f 199/228/199 214/238/214 213/252/213 198/226/198 +f 192/229/192 207/240/207 206/253/206 191/227/191 +f 204/249/204 219/254/219 218/255/218 203/245/203 +f 212/250/212 227/256/227 226/257/226 211/248/211 +f 205/251/205 220/258/220 219/254/219 204/249/204 +f 213/252/213 228/259/228 227/256/227 212/250/212 +f 206/253/206 221/260/221 220/258/220 205/251/205 +f 214/238/214 229/261/229 228/259/228 213/252/213 +f 207/240/207 222/262/222 221/260/221 206/253/206 +f 215/237/215 230/263/230 229/261/229 214/238/214 +f 208/239/208 223/264/223 222/262/222 207/240/207 +f 216/241/216 231/265/231 230/263/230 215/237/215 +f 209/242/209 224/266/224 223/264/223 208/239/208 +f 217/243/217 232/267/232 231/265/231 216/241/216 +f 210/244/210 225/268/225 224/266/224 209/242/209 +f 203/245/203 218/255/218 82/269/82 +f 308/270/308 232/267/232 217/243/217 +f 211/248/211 226/257/226 225/268/225 210/244/210 +f 223/264/223 238/271/238 237/272/237 222/262/222 +f 231/265/231 246/273/246 245/274/245 230/263/230 +f 224/266/224 239/275/239 238/271/238 223/264/223 +f 232/267/232 247/276/247 246/273/246 231/265/231 +f 225/268/225 240/277/240 239/275/239 224/266/224 +f 218/255/218 233/278/233 82/279/82 +f 308/280/308 247/276/247 232/267/232 +f 226/257/226 241/281/241 240/277/240 225/268/225 +f 219/254/219 234/282/234 233/278/233 218/255/218 +f 227/256/227 242/283/242 241/281/241 226/257/226 +f 220/258/220 235/284/235 234/282/234 219/254/219 +f 228/259/228 243/285/243 242/283/242 227/256/227 +f 221/260/221 236/286/236 235/284/235 220/258/220 +f 229/261/229 244/287/244 243/285/243 228/259/228 +f 222/262/222 237/272/237 236/286/236 221/260/221 +f 230/263/230 245/274/245 244/287/244 229/261/229 +f 242/283/242 257/288/257 256/289/256 241/281/241 +f 235/284/235 250/290/250 249/291/249 234/282/234 +f 243/285/243 258/292/258 257/288/257 242/283/242 +f 236/286/236 251/293/251 250/290/250 235/284/235 +f 244/287/244 259/294/259 258/292/258 243/285/243 +f 237/272/237 252/295/252 251/293/251 236/286/236 +f 245/274/245 260/296/260 259/294/259 244/287/244 +f 238/271/238 253/297/253 252/295/252 237/272/237 +f 246/273/246 261/298/261 260/296/260 245/274/245 +f 239/275/239 254/299/254 253/297/253 238/271/238 +f 247/276/247 262/300/262 261/298/261 246/273/246 +f 240/277/240 255/301/255 254/299/254 239/275/239 +f 233/278/233 248/302/248 82/303/82 +f 308/304/308 262/300/262 247/276/247 +f 241/281/241 256/289/256 255/301/255 240/277/240 +f 234/282/234 249/291/249 248/302/248 233/278/233 +f 261/298/261 276/305/276 275/306/275 260/296/260 +f 254/299/254 269/307/269 268/308/268 253/297/253 +f 262/300/262 277/309/277 276/305/276 261/298/261 +f 255/301/255 270/310/270 269/307/269 254/299/254 +f 248/302/248 263/311/263 82/312/82 +f 308/313/308 277/309/277 262/300/262 +f 256/289/256 271/314/271 270/310/270 255/301/255 +f 249/291/249 264/315/264 263/311/263 248/302/248 +f 257/288/257 272/316/272 271/314/271 256/289/256 +f 250/290/250 265/317/265 264/315/264 249/291/249 +f 258/292/258 273/318/273 272/316/272 257/288/257 +f 251/293/251 266/319/266 265/317/265 250/290/250 +f 259/294/259 274/320/274 273/318/273 258/292/258 +f 252/295/252 267/321/267 266/319/266 251/293/251 +f 260/296/260 275/306/275 274/320/274 259/294/259 +f 253/297/253 268/308/268 267/321/267 252/295/252 +f 273/318/273 288/322/288 287/323/287 272/316/272 +f 266/319/266 281/324/281 280/325/280 265/317/265 +f 274/320/274 289/326/289 288/322/288 273/318/273 +f 267/321/267 282/327/282 281/324/281 266/319/266 +f 275/306/275 290/328/290 289/326/289 274/320/274 +f 268/308/268 283/329/283 282/327/282 267/321/267 +f 276/305/276 291/330/291 290/328/290 275/306/275 +f 269/307/269 284/331/284 283/329/283 268/308/268 +f 277/309/277 292/332/292 291/330/291 276/305/276 +f 270/310/270 285/333/285 284/331/284 269/307/269 +f 263/311/263 278/334/278 82/335/82 +f 308/336/308 292/332/292 277/309/277 +f 271/314/271 286/337/286 285/333/285 270/310/270 +f 264/315/264 279/338/279 278/334/278 263/311/263 +f 272/316/272 287/323/287 286/337/286 271/314/271 +f 265/317/265 280/325/280 279/338/279 264/315/264 +f 292/332/292 307/339/307 306/340/306 291/330/291 +f 285/333/285 300/341/300 299/342/299 284/331/284 +f 278/334/278 293/343/293 82/344/82 +f 308/345/308 307/339/307 292/332/292 +f 286/337/286 301/346/301 300/341/300 285/333/285 +f 279/338/279 294/347/294 293/343/293 278/334/278 +f 287/323/287 302/348/302 301/346/301 286/337/286 +f 280/325/280 295/349/295 294/347/294 279/338/279 +f 288/322/288 303/350/303 302/348/302 287/323/287 +f 281/324/281 296/351/296 295/349/295 280/325/280 +f 289/326/289 304/352/304 303/350/303 288/322/288 +f 282/327/282 297/353/297 296/351/296 281/324/281 +f 290/328/290 305/354/305 304/352/304 289/326/289 +f 283/329/283 298/355/298 297/353/297 282/327/282 +f 291/330/291 306/340/306 305/354/305 290/328/290 +f 284/331/284 299/342/299 298/355/298 283/329/283 +f 296/351/296 312/356/312 311/357/311 295/349/295 +f 304/352/304 320/358/320 319/359/319 303/350/303 +f 297/353/297 313/360/313 312/356/312 296/351/296 +f 305/354/305 321/361/321 320/358/320 304/352/304 +f 298/355/298 314/362/314 313/360/313 297/353/297 +f 306/340/306 322/363/322 321/361/321 305/354/305 +f 299/342/299 315/364/315 314/362/314 298/355/298 +f 307/339/307 323/365/323 322/363/322 306/340/306 +f 300/341/300 316/366/316 315/364/315 299/342/299 +f 293/343/293 309/367/309 82/368/82 +f 308/369/308 323/365/323 307/339/307 +f 301/346/301 317/370/317 316/366/316 300/341/300 +f 294/347/294 310/371/310 309/367/309 293/343/293 +f 302/348/302 318/372/318 317/370/317 301/346/301 +f 295/349/295 311/357/311 310/371/310 294/347/294 +f 303/350/303 319/359/319 318/372/318 302/348/302 +f 316/366/316 331/373/331 330/374/330 315/364/315 +f 309/367/309 324/375/324 82/376/82 +f 308/377/308 338/378/338 323/365/323 +f 317/370/317 332/379/332 331/373/331 316/366/316 +f 310/371/310 325/380/325 324/375/324 309/367/309 +f 318/372/318 333/381/333 332/379/332 317/370/317 +f 311/357/311 326/382/326 325/380/325 310/371/310 +f 319/359/319 334/383/334 333/381/333 318/372/318 +f 312/356/312 327/384/327 326/382/326 311/357/311 +f 320/358/320 335/385/335 334/383/334 319/359/319 +f 313/360/313 328/386/328 327/384/327 312/356/312 +f 321/361/321 336/387/336 335/385/335 320/358/320 +f 314/362/314 329/388/329 328/386/328 313/360/313 +f 322/363/322 337/389/337 336/387/336 321/361/321 +f 315/364/315 330/374/330 329/388/329 314/362/314 +f 323/365/323 338/378/338 337/389/337 322/363/322 +f 335/385/335 350/390/350 349/391/349 334/383/334 +f 328/386/328 343/392/343 342/393/342 327/384/327 +f 336/387/336 351/394/351 350/390/350 335/385/335 +f 329/388/329 344/395/344 343/392/343 328/386/328 +f 337/389/337 352/396/352 351/394/351 336/387/336 +f 330/374/330 345/397/345 344/395/344 329/388/329 +f 338/378/338 353/398/353 352/396/352 337/389/337 +f 331/373/331 346/399/346 345/397/345 330/374/330 +f 324/375/324 339/400/339 82/401/82 +f 308/402/308 353/398/353 338/378/338 +f 332/379/332 347/403/347 346/399/346 331/373/331 +f 325/380/325 340/404/340 339/400/339 324/375/324 +f 333/381/333 348/405/348 347/403/347 332/379/332 +f 326/382/326 341/406/341 340/404/340 325/380/325 +f 334/383/334 349/391/349 348/405/348 333/381/333 +f 327/384/327 342/393/342 341/406/341 326/382/326 +f 339/400/339 354/407/354 82/408/82 +f 308/409/308 368/410/368 353/398/353 +f 347/403/347 362/411/362 361/412/361 346/399/346 +f 340/404/340 355/413/355 354/407/354 339/400/339 +f 348/405/348 363/414/363 362/411/362 347/403/347 +f 341/406/341 356/415/356 355/413/355 340/404/340 +f 349/391/349 364/416/364 363/414/363 348/405/348 +f 342/393/342 357/417/357 356/415/356 341/406/341 +f 350/390/350 365/418/365 364/416/364 349/391/349 +f 343/392/343 358/419/358 357/417/357 342/393/342 +f 351/394/351 366/420/366 365/418/365 350/390/350 +f 344/395/344 359/421/359 358/419/358 343/392/343 +f 352/396/352 367/422/367 366/420/366 351/394/351 +f 345/397/345 360/423/360 359/421/359 344/395/344 +f 353/398/353 368/410/368 367/422/367 352/396/352 +f 346/399/346 361/412/361 360/423/360 345/397/345 +f 358/424/358 373/425/373 372/426/372 357/427/357 +f 366/428/366 381/429/381 380/430/380 365/431/365 +f 359/432/359 374/433/374 373/425/373 358/424/358 +f 367/434/367 382/435/382 381/429/381 366/428/366 +f 360/436/360 375/437/375 374/433/374 359/432/359 +f 368/438/368 383/439/383 382/435/382 367/434/367 +f 361/440/361 376/441/376 375/437/375 360/436/360 +f 354/442/354 369/443/369 82/444/82 +f 308/445/308 383/439/383 368/438/368 +f 362/446/362 377/447/377 376/441/376 361/440/361 +f 355/448/355 370/449/370 369/443/369 354/442/354 +f 363/450/363 378/451/378 377/447/377 362/446/362 +f 356/452/356 371/453/371 370/449/370 355/448/355 +f 364/454/364 379/455/379 378/451/378 363/450/363 +f 357/427/357 372/426/372 371/453/371 356/452/356 +f 365/431/365 380/430/380 379/455/379 364/454/364 +f 377/447/377 392/456/392 391/457/391 376/441/376 +f 370/449/370 385/458/385 384/459/384 369/443/369 +f 378/451/378 393/460/393 392/456/392 377/447/377 +f 371/453/371 386/461/386 385/458/385 370/449/370 +f 379/455/379 394/462/394 393/460/393 378/451/378 +f 372/426/372 387/463/387 386/461/386 371/453/371 +f 380/430/380 395/464/395 394/462/394 379/455/379 +f 373/425/373 388/465/388 387/463/387 372/426/372 +f 381/429/381 396/466/396 395/464/395 380/430/380 +f 374/433/374 389/467/389 388/465/388 373/425/373 +f 382/435/382 397/468/397 396/466/396 381/429/381 +f 375/437/375 390/469/390 389/467/389 374/433/374 +f 383/439/383 398/470/398 397/468/397 382/435/382 +f 376/441/376 391/457/391 390/469/390 375/437/375 +f 369/443/369 384/459/384 82/471/82 +f 308/472/308 398/470/398 383/439/383 +f 396/466/396 411/473/411 410/474/410 395/464/395 +f 389/467/389 404/475/404 403/476/403 388/465/388 +f 397/468/397 412/477/412 411/473/411 396/466/396 +f 390/469/390 405/478/405 404/475/404 389/467/389 +f 398/470/398 413/479/413 412/477/412 397/468/397 +f 391/457/391 406/480/406 405/478/405 390/469/390 +f 384/459/384 399/481/399 82/482/82 +f 308/483/308 413/479/413 398/470/398 +f 392/456/392 407/484/407 406/480/406 391/457/391 +f 385/458/385 400/485/400 399/481/399 384/459/384 +f 393/460/393 408/486/408 407/484/407 392/456/392 +f 386/461/386 401/487/401 400/485/400 385/458/385 +f 394/462/394 409/488/409 408/486/408 393/460/393 +f 387/463/387 402/489/402 401/487/401 386/461/386 +f 395/464/395 410/474/410 409/488/409 394/462/394 +f 388/465/388 403/476/403 402/489/402 387/463/387 +f 408/486/408 423/490/423 422/491/422 407/484/407 +f 401/487/401 416/492/416 415/493/415 400/485/400 +f 409/488/409 424/494/424 423/490/423 408/486/408 +f 402/489/402 417/495/417 416/492/416 401/487/401 +f 410/474/410 425/496/425 424/494/424 409/488/409 +f 403/476/403 418/497/418 417/495/417 402/489/402 +f 411/473/411 426/498/426 425/496/425 410/474/410 +f 404/475/404 419/499/419 418/497/418 403/476/403 +f 412/477/412 427/500/427 426/498/426 411/473/411 +f 405/478/405 420/501/420 419/499/419 404/475/404 +f 413/479/413 428/502/428 427/500/427 412/477/412 +f 406/480/406 421/503/421 420/501/420 405/478/405 +f 399/481/399 414/504/414 82/505/82 +f 308/506/308 428/502/428 413/479/413 +f 407/484/407 422/491/422 421/503/421 406/480/406 +f 400/485/400 415/493/415 414/504/414 399/481/399 +f 427/500/427 442/507/442 441/508/441 426/498/426 +f 420/501/420 435/509/435 434/510/434 419/499/419 +f 428/502/428 443/511/443 442/507/442 427/500/427 +f 421/503/421 436/512/436 435/509/435 420/501/420 +f 414/504/414 429/513/429 82/514/82 +f 308/515/308 443/511/443 428/502/428 +f 422/491/422 437/516/437 436/512/436 421/503/421 +f 415/493/415 430/517/430 429/513/429 414/504/414 +f 423/490/423 438/518/438 437/516/437 422/491/422 +f 416/492/416 431/519/431 430/517/430 415/493/415 +f 424/494/424 439/520/439 438/518/438 423/490/423 +f 417/495/417 432/521/432 431/519/431 416/492/416 +f 425/496/425 440/522/440 439/520/439 424/494/424 +f 418/497/418 433/523/433 432/521/432 417/495/417 +f 426/498/426 441/508/441 440/522/440 425/496/425 +f 419/499/419 434/510/434 433/523/433 418/497/418 +f 431/519/431 446/524/446 445/525/445 430/517/430 +f 439/520/439 454/526/454 453/527/453 438/518/438 +f 432/521/432 447/528/447 446/524/446 431/519/431 +f 440/522/440 455/529/455 454/526/454 439/520/439 +f 433/523/433 448/530/448 447/528/447 432/521/432 +f 441/508/441 456/531/456 455/529/455 440/522/440 +f 434/510/434 449/532/449 448/530/448 433/523/433 +f 442/507/442 457/533/457 456/531/456 441/508/441 +f 435/509/435 450/534/450 449/532/449 434/510/434 +f 443/511/443 458/535/458 457/533/457 442/507/442 +f 436/512/436 451/536/451 450/534/450 435/509/435 +f 429/513/429 444/537/444 82/538/82 +f 308/539/308 458/535/458 443/511/443 +f 437/516/437 452/540/452 451/536/451 436/512/436 +f 430/517/430 445/525/445 444/537/444 429/513/429 +f 438/518/438 453/527/453 452/540/452 437/516/437 +f 450/534/450 465/541/465 464/542/464 449/532/449 +f 458/535/458 473/543/473 472/544/472 457/533/457 +f 451/536/451 466/545/466 465/541/465 450/534/450 +f 444/537/444 459/546/459 82/547/82 +f 308/548/308 473/543/473 458/535/458 +f 452/540/452 467/549/467 466/545/466 451/536/451 +f 445/525/445 460/550/460 459/546/459 444/537/444 +f 453/527/453 468/551/468 467/549/467 452/540/452 +f 446/524/446 461/552/461 460/550/460 445/525/445 +f 454/526/454 469/553/469 468/551/468 453/527/453 +f 447/528/447 462/554/462 461/552/461 446/524/446 +f 455/529/455 470/555/470 469/553/469 454/526/454 +f 448/530/448 463/556/463 462/554/462 447/528/447 +f 456/531/456 471/557/471 470/555/470 455/529/455 +f 449/532/449 464/542/464 463/556/463 448/530/448 +f 457/533/457 472/544/472 471/557/471 456/531/456 +f 469/553/469 6/5/6 478/8/478 468/551/468 +f 462/554/462 476/9/476 1/1/1 461/552/461 +f 470/555/470 479/11/479 6/5/6 469/553/469 +f 463/556/463 2/13/2 476/9/476 462/554/462 +f 471/557/471 480/15/480 479/11/479 470/555/470 +f 464/542/464 477/17/477 2/13/2 463/556/463 +f 472/544/472 481/19/481 480/15/480 471/557/471 +f 465/541/465 3/21/3 477/17/477 464/542/464 +f 473/543/473 482/23/482 481/19/481 472/544/472 +f 466/545/466 4/25/4 3/21/3 465/541/465 +f 459/546/459 474/27/474 82/558/82 +f 308/559/308 482/23/482 473/543/473 +f 467/549/467 5/31/5 4/25/4 466/545/466 +f 460/550/460 475/4/475 474/27/474 459/546/459 +f 468/551/468 478/8/478 5/31/5 467/549/467 +f 461/552/461 1/1/1 475/4/475 460/550/460 diff --git a/paperscope/assets/aurora.jpg b/paperscope/assets/aurora.jpg new file mode 100644 index 00000000..c8143491 Binary files /dev/null and b/paperscope/assets/aurora.jpg differ diff --git a/paperscope/assets/button_animation_0001.png b/paperscope/assets/button_animation_0001.png new file mode 100644 index 00000000..b0020920 Binary files /dev/null and b/paperscope/assets/button_animation_0001.png differ diff --git a/paperscope/assets/button_animation_0002.png b/paperscope/assets/button_animation_0002.png new file mode 100644 index 00000000..2fec4784 Binary files /dev/null and b/paperscope/assets/button_animation_0002.png differ diff --git a/paperscope/assets/button_animation_0003.png b/paperscope/assets/button_animation_0003.png new file mode 100644 index 00000000..22c344e5 Binary files /dev/null and b/paperscope/assets/button_animation_0003.png differ diff --git a/paperscope/assets/button_animation_0004.png b/paperscope/assets/button_animation_0004.png new file mode 100644 index 00000000..2f6c0413 Binary files /dev/null and b/paperscope/assets/button_animation_0004.png differ diff --git a/paperscope/assets/button_animation_0005.png b/paperscope/assets/button_animation_0005.png new file mode 100644 index 00000000..eb05dfc3 Binary files /dev/null and b/paperscope/assets/button_animation_0005.png differ diff --git a/paperscope/assets/button_animation_0006.png b/paperscope/assets/button_animation_0006.png new file mode 100644 index 00000000..462e99d9 Binary files /dev/null and b/paperscope/assets/button_animation_0006.png differ diff --git a/paperscope/assets/button_animation_0007.png b/paperscope/assets/button_animation_0007.png new file mode 100644 index 00000000..a9c49b9a Binary files /dev/null and b/paperscope/assets/button_animation_0007.png differ diff --git a/paperscope/assets/button_animation_0008.png b/paperscope/assets/button_animation_0008.png new file mode 100644 index 00000000..121cf18d Binary files /dev/null and b/paperscope/assets/button_animation_0008.png differ diff --git a/paperscope/assets/button_animation_0009.png b/paperscope/assets/button_animation_0009.png new file mode 100644 index 00000000..72bfbeb9 Binary files /dev/null and b/paperscope/assets/button_animation_0009.png differ diff --git a/paperscope/assets/button_animation_0010.png b/paperscope/assets/button_animation_0010.png new file mode 100644 index 00000000..bdc50622 Binary files /dev/null and b/paperscope/assets/button_animation_0010.png differ diff --git a/paperscope/assets/button_animation_0011.png b/paperscope/assets/button_animation_0011.png new file mode 100644 index 00000000..f55e6174 Binary files /dev/null and b/paperscope/assets/button_animation_0011.png differ diff --git a/paperscope/assets/button_animation_0012.png b/paperscope/assets/button_animation_0012.png new file mode 100644 index 00000000..81bbdd20 Binary files /dev/null and b/paperscope/assets/button_animation_0012.png differ diff --git a/paperscope/assets/button_animation_0013.png b/paperscope/assets/button_animation_0013.png new file mode 100644 index 00000000..1967a33d Binary files /dev/null and b/paperscope/assets/button_animation_0013.png differ diff --git a/paperscope/assets/button_animation_0014.png b/paperscope/assets/button_animation_0014.png new file mode 100644 index 00000000..7372abdd Binary files /dev/null and b/paperscope/assets/button_animation_0014.png differ diff --git a/paperscope/assets/button_animation_0015.png b/paperscope/assets/button_animation_0015.png new file mode 100644 index 00000000..df6db5b7 Binary files /dev/null and b/paperscope/assets/button_animation_0015.png differ diff --git a/paperscope/assets/exhibit-bw.png b/paperscope/assets/exhibit-bw.png new file mode 100644 index 00000000..1000b470 Binary files /dev/null and b/paperscope/assets/exhibit-bw.png differ diff --git a/paperscope/assets/exhibit-color.png b/paperscope/assets/exhibit-color.png new file mode 100644 index 00000000..6672e263 Binary files /dev/null and b/paperscope/assets/exhibit-color.png differ diff --git a/paperscope/assets/home_mono.jpg b/paperscope/assets/home_mono.jpg new file mode 100644 index 00000000..e645fd6e Binary files /dev/null and b/paperscope/assets/home_mono.jpg differ diff --git a/paperscope/assets/mymovies-bw.png b/paperscope/assets/mymovies-bw.png new file mode 100644 index 00000000..0ba004ee Binary files /dev/null and b/paperscope/assets/mymovies-bw.png differ diff --git a/paperscope/assets/mymovies-color.png b/paperscope/assets/mymovies-color.png new file mode 100644 index 00000000..819c9647 Binary files /dev/null and b/paperscope/assets/mymovies-color.png differ diff --git a/paperscope/assets/photospheres-bw.png b/paperscope/assets/photospheres-bw.png new file mode 100644 index 00000000..873a99d8 Binary files /dev/null and b/paperscope/assets/photospheres-bw.png differ diff --git a/paperscope/assets/photospheres-color.png b/paperscope/assets/photospheres-color.png new file mode 100644 index 00000000..2c712b6a Binary files /dev/null and b/paperscope/assets/photospheres-color.png differ diff --git a/paperscope/assets/tilt_animation_0000.png b/paperscope/assets/tilt_animation_0000.png new file mode 100644 index 00000000..03829243 Binary files /dev/null and b/paperscope/assets/tilt_animation_0000.png differ diff --git a/paperscope/assets/tilt_animation_0001.png b/paperscope/assets/tilt_animation_0001.png new file mode 100644 index 00000000..de6db6a9 Binary files /dev/null and b/paperscope/assets/tilt_animation_0001.png differ diff --git a/paperscope/assets/tilt_animation_0002.png b/paperscope/assets/tilt_animation_0002.png new file mode 100644 index 00000000..58552797 Binary files /dev/null and b/paperscope/assets/tilt_animation_0002.png differ diff --git a/paperscope/assets/tilt_animation_0003.png b/paperscope/assets/tilt_animation_0003.png new file mode 100644 index 00000000..34b568c4 Binary files /dev/null and b/paperscope/assets/tilt_animation_0003.png differ diff --git a/paperscope/assets/tilt_animation_0004.png b/paperscope/assets/tilt_animation_0004.png new file mode 100644 index 00000000..c7126d9f Binary files /dev/null and b/paperscope/assets/tilt_animation_0004.png differ diff --git a/paperscope/assets/tilt_animation_0005.png b/paperscope/assets/tilt_animation_0005.png new file mode 100644 index 00000000..e3f47298 Binary files /dev/null and b/paperscope/assets/tilt_animation_0005.png differ diff --git a/paperscope/assets/tilt_animation_0006.png b/paperscope/assets/tilt_animation_0006.png new file mode 100644 index 00000000..af203e9c Binary files /dev/null and b/paperscope/assets/tilt_animation_0006.png differ diff --git a/paperscope/assets/tilt_animation_0007.png b/paperscope/assets/tilt_animation_0007.png new file mode 100644 index 00000000..0c7699ca Binary files /dev/null and b/paperscope/assets/tilt_animation_0007.png differ diff --git a/paperscope/assets/tilt_animation_0008.png b/paperscope/assets/tilt_animation_0008.png new file mode 100644 index 00000000..c76a7dbf Binary files /dev/null and b/paperscope/assets/tilt_animation_0008.png differ diff --git a/paperscope/assets/tilt_animation_0009.png b/paperscope/assets/tilt_animation_0009.png new file mode 100644 index 00000000..ee51cd40 Binary files /dev/null and b/paperscope/assets/tilt_animation_0009.png differ diff --git a/paperscope/assets/tilt_animation_0010.png b/paperscope/assets/tilt_animation_0010.png new file mode 100644 index 00000000..f6e895f7 Binary files /dev/null and b/paperscope/assets/tilt_animation_0010.png differ diff --git a/paperscope/assets/tilt_animation_0011.png b/paperscope/assets/tilt_animation_0011.png new file mode 100644 index 00000000..dd788e41 Binary files /dev/null and b/paperscope/assets/tilt_animation_0011.png differ diff --git a/paperscope/assets/tilt_animation_0012.png b/paperscope/assets/tilt_animation_0012.png new file mode 100644 index 00000000..bace3c4f Binary files /dev/null and b/paperscope/assets/tilt_animation_0012.png differ diff --git a/paperscope/assets/tilt_animation_0013.png b/paperscope/assets/tilt_animation_0013.png new file mode 100644 index 00000000..02c6e044 Binary files /dev/null and b/paperscope/assets/tilt_animation_0013.png differ diff --git a/paperscope/assets/tilt_animation_0014.png b/paperscope/assets/tilt_animation_0014.png new file mode 100644 index 00000000..558ffd78 Binary files /dev/null and b/paperscope/assets/tilt_animation_0014.png differ diff --git a/paperscope/assets/tutorial-bw.png b/paperscope/assets/tutorial-bw.png new file mode 100644 index 00000000..88166fcd Binary files /dev/null and b/paperscope/assets/tutorial-bw.png differ diff --git a/paperscope/assets/tutorial-color.png b/paperscope/assets/tutorial-color.png new file mode 100644 index 00000000..a04be4b7 Binary files /dev/null and b/paperscope/assets/tutorial-color.png differ diff --git a/paperscope/build.gradle b/paperscope/build.gradle new file mode 100644 index 00000000..7c9cf848 --- /dev/null +++ b/paperscope/build.gradle @@ -0,0 +1,104 @@ +apply plugin: 'com.android.application' + +android { + compileSdk = 35 + lintOptions { + abortOnError = false + } + defaultConfig { + applicationId "com.google.cardboard.paperscope" + // #gles3 #vulkan - You can reduce minSdkVersion down to 16 depending + // on rendering APIs supported: + // + // OpenGL ES 2.0 - Requires 16 or higher + // OpenGL ES 3.0 - Requires 18 or higher + // Vulkan - Requires 24 or higher + // + // See the release notes for details. + minSdkVersion = 26 + targetSdkVersion = 35 + versionCode 1 + versionName "1.33.0" + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' + ndk { + abiFilters 'armeabi-v7a', 'arm64-v8a' + } + externalNativeBuild.cmake { + arguments "-DANDROID_STL=c++_shared" + } + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + externalNativeBuild.cmake { + path "CMakeLists.txt" + } + namespace = 'com.google.vr.cardboard.paperscope.demo' + + sourceSets { + main { + manifest.srcFile 'AndroidManifest.xml' + java.srcDirs = [ + 'about', + 'common', + 'core', + 'exhibit', + 'magicwindow', + 'myvideos', + 'photosphere', + 'settings', + ] + res.srcDirs = ['res', 'about/res', 'common/res', 'myvideos/res', 'settings/res', 'magicwindow/res'] + assets.srcDirs = ['assets', 'photosphere/assets', 'exhibit/assets'] + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'androidx.appcompat:appcompat:1.7.1' + // Android Mobile Vision + // TODO(b/213613345) Migrate to ML Kit. + implementation 'com.google.android.gms:play-services-vision:20.1.3' + implementation 'com.google.android.material:material:1.13.0' + implementation 'com.google.protobuf:protobuf-javalite:4.34.1' + implementation "com.google.flogger:flogger:0.9" + implementation 'com.google.flogger:flogger-system-backend:0.9' + implementation 'com.google.guava:guava:33.5.0-android' + implementation 'com.adobe.xmp:xmpcore:5.1.3' + implementation 'androidx.palette:palette:1.0.0' + implementation project(":sdk") +} + +// The dependencies for NDK builds live inside the .aar files so they need to +// be extracted before NDK targets can link against. +task extractNdk { + dependsOn ':sdk:assembleRelease' + doLast { + copy { + from zipTree("${project.rootDir}/sdk/build/outputs/aar/sdk-release.aar") + into "libraries/" + include "jni/**/libGfxPluginCardboard.so" + } + copy { + from "${project.rootDir}/sdk/include/cardboard.h" + into "libraries/" + } + } +} + +task deleteNdk(type: Delete) { + delete "libraries/jni" + delete "libraries/cardboard.h" +} + +preBuild.dependsOn(extractNdk) +clean.dependsOn(deleteNdk) diff --git a/paperscope/common/AndroidManifest.xml b/paperscope/common/AndroidManifest.xml new file mode 100644 index 00000000..f8d52fa1 --- /dev/null +++ b/paperscope/common/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/paperscope/common/DemoBaseActivity.java b/paperscope/common/DemoBaseActivity.java new file mode 100644 index 00000000..c554e9de --- /dev/null +++ b/paperscope/common/DemoBaseActivity.java @@ -0,0 +1,82 @@ +package com.google.vr.cardboard.paperscope.demo.common; + +import com.google.vr.cardboard.paperscope.demo.R; + +import android.content.Intent; +import android.os.Bundle; +import androidx.appcompat.app.AppCompatActivity; +import android.view.OrientationEventListener; +import com.google.cardboard.sdk.CardboardView; +import com.google.cardboard.sdk.CardboardView.Renderer; +import com.google.common.flogger.FluentLogger; +import com.google.vr.cardboard.paperscope.demo.settings.SettingsActivity; + +/** Base activity class for the demos. */ +public abstract class DemoBaseActivity extends AppCompatActivity { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + protected DemoBaseController demoActivityController; + private OrientationEventListener orientationEventListener; + protected CardboardView cardboardView; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.demo_base_hud); + cardboardView = findViewById(R.id.cardboard_view); + + demoActivityController = createController(); + initializeOrientationEventListener(); + + demoActivityController.setOnCloseButtonListener(this::finish); + demoActivityController.setOnSettingsButtonListener(this::showSettings); + demoActivityController.enableOnTriggerEventListener(); + } + + @Override + protected void onResume() { + super.onResume(); + demoActivityController.onResume(); + + if (orientationEventListener != null) { + orientationEventListener.enable(); + } + } + + @Override + protected void onPause() { + super.onPause(); + demoActivityController.onPause(); + + if (orientationEventListener != null) { + orientationEventListener.disable(); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + demoActivityController.shutdown(); + orientationEventListener = null; + } + + protected abstract DemoBaseController createController(); + + private void initializeOrientationEventListener() { + orientationEventListener = + new OrientationEventListener(this) { + @Override + public void onOrientationChanged(int orientation) { + if (demoActivityController != null + && demoActivityController.shouldFinishActivity(orientation)) { + logger.atInfo().log("Finishing activity due to orientation change."); + finish(); + } + } + }; + } + + private void showSettings() { + Intent intent = new Intent(this, SettingsActivity.class); + startActivity(intent); + } +} diff --git a/paperscope/common/DemoBaseController.java b/paperscope/common/DemoBaseController.java new file mode 100644 index 00000000..bc4b64c9 --- /dev/null +++ b/paperscope/common/DemoBaseController.java @@ -0,0 +1,107 @@ +package com.google.vr.cardboard.paperscope.demo.common; + +import static java.lang.Math.min; + +import android.view.OrientationEventListener; +import com.google.cardboard.sdk.CardboardView; +import com.google.cardboard.sdk.CardboardView.Renderer; + +/** Controller for the demo activity. */ +public class DemoBaseController { + private static final String TAG = DemoBaseController.class.getSimpleName(); + protected final CardboardView cardboardView; + protected T renderer; + private static final int TILT_DETECTION_TOLERANCE_IN_DEGREES = 30; + private static final int LANDSCAPE_LEFT_DEGREES = 270; + private static final int LANDSCAPE_RIGHT_DEGREES = 90; + + public DemoBaseController(CardboardView cardboardView) { + this.cardboardView = cardboardView; + CardboardView.setUseCardboardGlSurfaceView(true); + this.cardboardView.setOnViewDetachedRunnable(this::shutdown); + this.cardboardView.setStereoRenderMode(true); + } + + public void setOnCloseButtonListener(Runnable onCloseButtonListener) { + cardboardView.setOnBackButtonClick(onCloseButtonListener); + } + + public void setOnSettingsButtonListener(Runnable onSettingsButtonListener) { + cardboardView.setOnSettingsButtonClick(onSettingsButtonListener); + } + + public void enableOnTriggerEventListener() {} + + public void onResume() { + cardboardView.onResume(); + } + + public void onPause() { + cardboardView.onPause(); + } + + public void shutdown() { + if (renderer != null) { + renderer.onRendererShutdown(); + } + cardboardView.onDestroy(); + } + + /** + * Determines whether the activity should be finished based on the device's orientation. + * + * @param orientation The current orientation of the device in degrees. + * @return True if the activity should be finished, false otherwise. + */ + public boolean shouldFinishActivity(int orientation) { + if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN) { + return false; + } + + return !isLandscape(orientation); + } + + /** + * Checks if the device is in a landscape orientation (left or right). + * + * @param orientationDegrees The current orientation of the device in degrees. + * @return True if the device is in a landscape orientation, false otherwise. + */ + private boolean isLandscape(int orientationDegrees) { + return isLandscapeLeft(orientationDegrees) || isLandscapeRight(orientationDegrees); + } + + /** + * Checks if the device is in a left landscape orientation. + * + * @param orientationDegrees The current orientation of the device in degrees. + * @return True if the device is in a left landscape orientation, false otherwise. + */ + private boolean isLandscapeLeft(int orientationDegrees) { + return getCircularDistance(orientationDegrees, LANDSCAPE_LEFT_DEGREES) + < TILT_DETECTION_TOLERANCE_IN_DEGREES; + } + + /** + * Checks if the device is in a right landscape orientation. + * + * @param orientationDegrees The current orientation of the device in degrees. + * @return True if the device is in a right landscape orientation, false otherwise. + */ + private boolean isLandscapeRight(int orientationDegrees) { + return getCircularDistance(orientationDegrees, LANDSCAPE_RIGHT_DEGREES) + < TILT_DETECTION_TOLERANCE_IN_DEGREES; + } + + /** + * Returns the shortest distance between two angles on a circle. + * + * @param angle1 The first angle, in degrees. + * @param angle2 The second angle, in degrees. + * @return The shortest distance, in degrees. + */ + private int getCircularDistance(int angle1, int angle2) { + int diff = Math.abs(angle1 - angle2); + return min(diff, 360 - diff); + } +} diff --git a/paperscope/common/res/layout/demo_base_hud.xml b/paperscope/common/res/layout/demo_base_hud.xml new file mode 100644 index 00000000..85186011 --- /dev/null +++ b/paperscope/common/res/layout/demo_base_hud.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/paperscope/core/AppGridView.java b/paperscope/core/AppGridView.java new file mode 100644 index 00000000..c5d98489 --- /dev/null +++ b/paperscope/core/AppGridView.java @@ -0,0 +1,43 @@ +package com.google.vr.cardboard.paperscope.demo; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.GridView; + +/** + * This class holds the {@link CardView} elements that are displayed in the {@link MainActivity}. + * + *

Customized {@link GridView} that correctly sets its height to {@link + * android.view.ViewGroup.LayoutParams#WRAP_CONTENT}. + * + * @see Related discussion on StackOverflow. + */ +public class AppGridView extends GridView { + public AppGridView(Context context) { + super(context); + } + + public AppGridView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public AppGridView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int heightSpec; + + if (getLayoutParams().height == LayoutParams.WRAP_CONTENT) { + // The two leftmost bits in the height measure spec have + // a special meaning, hence we can't use them to describe height. + heightSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST); + } else { + // Any other height should be respected as is. + heightSpec = heightMeasureSpec; + } + + super.onMeasure(widthMeasureSpec, heightSpec); + } +} diff --git a/paperscope/core/BasicAppInfo.java b/paperscope/core/BasicAppInfo.java new file mode 100644 index 00000000..5b7f428b --- /dev/null +++ b/paperscope/core/BasicAppInfo.java @@ -0,0 +1,57 @@ +package com.google.vr.cardboard.paperscope.demo; + +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import androidx.palette.graphics.Palette; + +/** + * This class holds the information necessary for constructing the supported apps list in the {@link + * GridView}. + */ +public class BasicAppInfo { + private static final int ICON_SIZE_PIXELS = 100; + private String packageName; + private String appName; + private Intent startIntent; + private Drawable icon; + private Palette palette; + + public BasicAppInfo(String packageName, String appName, Intent startIntent, Drawable icon) { + this.packageName = packageName; + this.appName = appName; + this.startIntent = startIntent; + this.icon = icon; + this.palette = + Palette.from(convertToBitmap(icon, ICON_SIZE_PIXELS, ICON_SIZE_PIXELS)).generate(); + } + + private Bitmap convertToBitmap(Drawable drawable, int widthPixels, int heightPixels) { + Bitmap mutableBitmap = Bitmap.createBitmap(widthPixels, heightPixels, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(mutableBitmap); + drawable.setBounds(0, 0, widthPixels, heightPixels); + drawable.draw(canvas); + return mutableBitmap; + } + + public String getPackageName() { + return packageName; + } + + public String getAppName() { + return appName; + } + + public Intent getStartIntent() { + return startIntent; + } + + public Drawable getIcon() { + return icon; + } + + public Palette getPalette() { + return palette; + } +} diff --git a/paperscope/core/CardboardLauncherActivity.java b/paperscope/core/CardboardLauncherActivity.java new file mode 100644 index 00000000..06fcba9e --- /dev/null +++ b/paperscope/core/CardboardLauncherActivity.java @@ -0,0 +1,256 @@ +package com.google.vr.cardboard.paperscope.demo; + +import com.google.vr.cardboard.paperscope.demo.R; + +import android.annotation.SuppressLint; +import android.content.Intent; +import android.content.res.AssetManager; +import android.opengl.GLSurfaceView; +import android.os.Bundle; +import androidx.appcompat.app.AppCompatActivity; +import android.util.Log; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.FrameLayout; +import android.widget.ImageButton; +import android.widget.PopupMenu; +import androidx.core.view.WindowInsetsCompat; +import androidx.core.view.WindowInsetsControllerCompat; +import com.google.vr.cardboard.paperscope.demo.settings.SettingsActivity; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +/** + * A Google Cardboard demo application. + * + *

This is the main Activity for the demo application. It initializes a GLSurfaceView to allow + * rendering. + */ +public class CardboardLauncherActivity extends AppCompatActivity + implements PopupMenu.OnMenuItemClickListener { + static { + System.loadLibrary("cardboard_jni"); + } + + private static final String TAG = CardboardLauncherActivity.class.getSimpleName(); + + // Opaque native pointer to the native CardboardApp instance. + // This object is owned by the CardboardLauncherActivity instance + // and passed to the native methods. + private long nativeApp; + private TransitionView transitionView; + private TransitionView.Listener transitionViewListener; + private GLSurfaceView glView; + + @SuppressLint("ClickableViewAccessibility") + @Override + public void onCreate(Bundle savedInstance) { + super.onCreate(savedInstance); + + nativeApp = nativeOnCreate(getAssets()); + + setContentView(R.layout.activity_vr); + glView = findViewById(R.id.surface_view); + glView.setEGLContextClientVersion(2); + Renderer renderer = new Renderer(); + glView.setRenderer(renderer); + glView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY); + glView.setOnTouchListener( + (v, event) -> { + if (nativeShouldLaunchDemo(nativeApp)) { + launchDemo(nativeDemoToLaunch(nativeApp)); + } + if (event.getAction() == MotionEvent.ACTION_DOWN) { + // Signal a trigger event. + glView.queueEvent( + () -> { + nativeOnTriggerEvent(nativeApp); + }); + return true; + } + return false; + }); + + // TODO(b/139010241): Avoid that action and status bar are displayed when pressing settings + // button. + setImmersiveSticky(); + + // Forces screen to max brightness. + WindowManager.LayoutParams layout = getWindow().getAttributes(); + layout.screenBrightness = 1.f; + getWindow().setAttributes(layout); + + // Prevents screen from dimming/locking. + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + + // Buttons listeners for close and settings. + ImageButton closeButton = findViewById(R.id.ui_back_button); + closeButton.setOnClickListener( + (view) -> { + Log.d(TAG, "Close button clicked"); + closeSample(); + }); + ImageButton settingsButton = findViewById(R.id.ui_settings_button); + settingsButton.setOnClickListener( + (view) -> { + Log.d(TAG, "Settings button clicked"); + showSettings(view); + }); + + // Create TransitionView Listener to pass to the Transition View + transitionViewListener = + new TransitionView.Listener() { + @Override + public void onSwitchButtonClicked() { + Intent intent = new Intent(CardboardLauncherActivity.this, SettingsActivity.class); + startActivity(intent); + } + + @Override + public void onExitButtonClicked() { + closeSample(); + } + }; + + // Create the full-screen custom view + transitionView = new TransitionView(this, transitionViewListener); + + // Set layout params to fill the screen + FrameLayout.LayoutParams params = + new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); + + // Add the TransitionView custom view to the activity + addContentView(transitionView, params); + } + + @Override + protected void onPause() { + super.onPause(); + nativeOnPause(nativeApp); + glView.onPause(); + } + + @Override + protected void onResume() { + super.onResume(); + + glView.onResume(); + nativeOnResume(nativeApp); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + nativeOnDestroy(nativeApp); + nativeApp = 0; + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + if (hasFocus) { + setImmersiveSticky(); + } + } + + private class Renderer implements GLSurfaceView.Renderer { + @Override + public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) { + nativeOnSurfaceCreated(nativeApp); + } + + @Override + public void onSurfaceChanged(GL10 gl10, int width, int height) { + nativeSetScreenParams(nativeApp, width, height); + } + + @Override + public void onDrawFrame(GL10 gl10) { + nativeOnDrawFrame(nativeApp); + } + } + + /** Callback for when close button is pressed. */ + public void closeSample() { + Log.d(TAG, "Leaving VR sample"); + finish(); + } + + /** Callback for when settings_menu button is pressed. */ + public void showSettings(View view) { + PopupMenu popup = new PopupMenu(this, view); + MenuInflater inflater = popup.getMenuInflater(); + inflater.inflate(R.menu.settings_menu, popup.getMenu()); + popup.setOnMenuItemClickListener(this); + popup.show(); + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + if (item.getItemId() == R.id.switch_viewer) { + switchViewer(); + return true; + } + return false; + } + + private void setImmersiveSticky() { + WindowInsetsControllerCompat controller = + new WindowInsetsControllerCompat(getWindow(), getWindow().getDecorView()); + controller.hide(WindowInsetsCompat.Type.systemBars()); + controller.setSystemBarsBehavior( + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + } + + private void launchDemo(String demoName) { + String className = ""; + if (demoName.equals("exhibit")) { + className = "com.google.vr.cardboard.paperscope.demo.exhibit.ExhibitActivity"; + } else if (demoName.equals("mymovies")) { + className = "com.google.vr.cardboard.paperscope.demo.myvideos.MyVideosActivity"; + } else if (demoName.equals("photospheres")) { + className = "com.google.vr.cardboard.paperscope.demo.photosphere.PhotoSphereActivity"; + } + try { + Class activityClass = Class.forName(className); + Intent intent = new Intent(this, activityClass); + startActivity(intent); + nativeDemoLaunched(nativeApp); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + } + + public void switchViewer() { + nativeSwitchViewer(nativeApp); + } + + private native long nativeOnCreate(AssetManager assetManager); + + private native void nativeOnDestroy(long nativeApp); + + private native void nativeOnSurfaceCreated(long nativeApp); + + private native void nativeOnDrawFrame(long nativeApp); + + private native void nativeOnTriggerEvent(long nativeApp); + + private native void nativeOnPause(long nativeApp); + + private native void nativeOnResume(long nativeApp); + + private native void nativeSetScreenParams(long nativeApp, int width, int height); + + private native void nativeSwitchViewer(long nativeApp); + + private native boolean nativeShouldLaunchDemo(long nativeApp); + + private native String nativeDemoToLaunch(long nativeApp); + + private native void nativeDemoLaunched(long nativeApp); +} diff --git a/paperscope/core/MainActivity.java b/paperscope/core/MainActivity.java new file mode 100644 index 00000000..06126a50 --- /dev/null +++ b/paperscope/core/MainActivity.java @@ -0,0 +1,208 @@ +package com.google.vr.cardboard.paperscope.demo; + +import android.content.Intent; +import android.content.res.AssetManager; +import android.net.Uri; +import android.os.Bundle; +import androidx.fragment.app.Fragment; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import android.view.Menu; +import android.view.MenuItem; +import android.view.ViewGroup.MarginLayoutParams; +import android.widget.Toast; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; +import androidx.viewpager2.adapter.FragmentStateAdapter; +import androidx.viewpager2.widget.ViewPager2; +import com.google.android.material.appbar.AppBarLayout; +import com.google.android.material.appbar.AppBarLayout.Behavior; +import com.google.android.material.tabs.TabLayout; +import com.google.android.material.tabs.TabLayoutMediator; +import com.google.cardboard.sdk.CardboardViewApi; +import com.google.vr.cardboard.paperscope.demo.about.AboutActivity; +import com.google.vr.cardboard.paperscope.demo.magicwindow.MagicWindowActivity; +import org.jspecify.annotations.Nullable; + +/** + * This is the Activity where the user can see a list of Cardboard compatible apps to launch, along + * with the provided Cardboard Demo. + */ +public class MainActivity extends AppCompatActivity { + static { + System.loadLibrary("cardboard_jni"); + } + + private long nativeApp; + + private static final String TAG = MainActivity.class.getSimpleName(); + + /** Index of MyLibrary fragment in the tab. */ + private static final int TAB_INDEX_MY_LIBRARY = 0; + + /** Number of fragments in the tab. */ + private static final int NUM_FRAGMENTS = 1; + + /** The URL for the Google Privacy Policy. */ + private static final Uri GOOGLE_PRIVACY_URL = + Uri.parse("http://www.google.com/policies/privacy/"); + + /** The URL for the Google Cardboard Contact Us page. */ + private static final Uri GOOGLE_CONTACT_URL = + Uri.parse( + "https://support.google.com/cardboard?hl=en&sjid=7565740170206509904-NC#topic=6295044"); + + private CoordinatorLayout coordinatorLayout; + private CoordinatorLayout.LayoutParams layoutParams; + private AppBarLayout appBarLayout; + private AppBarLayout.Behavior behavior; + private ViewPager2 pager; + + private CardboardViewApi cardboardViewApi; + + public static T checkNotNull(@Nullable T reference) { + if (reference == null) { + throw new NullPointerException(); + } + return reference; + } + + @Override + public void onCreate(Bundle savedInstance) { + super.onCreate(savedInstance); + + nativeApp = nativeOnCreate(getAssets()); + + setContentView(R.layout.my_library); + + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + coordinatorLayout = checkNotNull((CoordinatorLayout) findViewById(R.id.coordinator)); + appBarLayout = checkNotNull((AppBarLayout) findViewById(R.id.appbar)); + layoutParams = (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams(); + layoutParams.setBehavior(new Behavior()); + behavior = (AppBarLayout.Behavior) checkNotNull(layoutParams.getBehavior()); + + ViewCompat.setOnApplyWindowInsetsListener( + coordinatorLayout, + (v, windowInsets) -> { + Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()); + + MarginLayoutParams mlp = (MarginLayoutParams) v.getLayoutParams(); + mlp.leftMargin = insets.left; + mlp.bottomMargin = insets.bottom; + mlp.topMargin = insets.top; + mlp.rightMargin = insets.right; + v.setLayoutParams(mlp); + + return WindowInsetsCompat.CONSUMED; + }); + + pager = checkNotNull((ViewPager2) findViewById(R.id.pager)); + pager.setAdapter(new TabsAdapter(this)); + pager.registerOnPageChangeCallback( + new ViewPager2.OnPageChangeCallback() { + @Override + public void onPageSelected(int position) { + // Reset toolbar's position when switching pages. This is done by simulating a quick + // fling. + behavior.onNestedFling( + /* coordinatorLayout= */ coordinatorLayout, + /* child= */ appBarLayout, + /* target= */ null, + /* velocityX= */ 0, + /* velocityY= */ -1000, + /* consumed= */ true); + } + }); + + TabLayout tabLayout = checkNotNull((TabLayout) findViewById(R.id.tabs)); + new TabLayoutMediator( + tabLayout, + pager, + (tab, position) -> { + tab.setText( + switch (position) { + case TAB_INDEX_MY_LIBRARY -> getString(R.string.app_grid_view_name); + default -> + throw new IllegalArgumentException( + "Invalid fragment index. index=" + position); + }); + }) + .attach(); + + cardboardViewApi = new CardboardViewApi(this); + } + + @Override + public void onResume() { + super.onResume(); + + if (!cardboardViewApi.hasSavedDeviceParams()) { + Intent intent = new Intent(this, MagicWindowActivity.class); + startActivity(intent); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.my_library_settings_menu, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + if (id == R.id.action_switch_viewer) { + nativeSwitchViewer(nativeApp); + return true; + } else if (id == R.id.action_about) { + startActivity(new Intent(this, AboutActivity.class)); + return true; + } else if (id == R.id.action_privacy_policy) { + try { + startActivity(new Intent(Intent.ACTION_VIEW, GOOGLE_PRIVACY_URL)); + } catch (android.content.ActivityNotFoundException e) { + Toast.makeText(this, R.string.view_web_page_failed, Toast.LENGTH_SHORT).show(); + } + return true; + } else if (id == R.id.contact_us) { + try { + startActivity(new Intent(Intent.ACTION_VIEW, GOOGLE_CONTACT_URL)); + } catch (android.content.ActivityNotFoundException e) { + Toast.makeText(this, R.string.view_web_page_failed, Toast.LENGTH_SHORT).show(); + } + return true; + } + return super.onOptionsItemSelected(item); + } + + private native long nativeOnCreate(AssetManager assetManager); + + private native void nativeSwitchViewer(long nativeApp); + + /** The adapter for the two fragments. */ + private static class TabsAdapter extends FragmentStateAdapter { + + TabsAdapter(AppCompatActivity activity) { + super(activity); + } + + @Override + public Fragment createFragment(int index) { + return switch (index) { + case TAB_INDEX_MY_LIBRARY -> new MyLibraryFragment(); + default -> throw new IllegalArgumentException("Invalid fragment index. index=" + index); + }; + } + + @Override + public int getItemCount() { + return NUM_FRAGMENTS; + } + } +} diff --git a/paperscope/core/MyLibraryFragment.java b/paperscope/core/MyLibraryFragment.java new file mode 100644 index 00000000..d9f26a91 --- /dev/null +++ b/paperscope/core/MyLibraryFragment.java @@ -0,0 +1,134 @@ +package com.google.vr.cardboard.paperscope.demo; + +import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Bundle; +import androidx.fragment.app.Fragment; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.GridView; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.palette.graphics.Palette; +import java.util.List; + +/** Fragment where the list of Cardboard supported apps is generated and displayed to the user. */ +public class MyLibraryFragment extends Fragment { + private static final String LOG_TAG = MyLibraryFragment.class.getName(); + private static final int DEMO_ICON_PADDING_COLOR = 0xFFE0E0E0; + private PackageManager packageManager; + private GridView gridView; + private AppAdapter adapter; + private Activity activity; + private SupportedAppProvider supportedAppProvider; + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.my_library_fragment, container, false); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + + try { + activity = (MainActivity) context; + } catch (ClassCastException e) { + ClassCastException exception = + new ClassCastException(context.toString() + " must be an instance of MainActivity"); + exception.initCause(e); + throw exception; + } + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + packageManager = getContext().getPackageManager(); + supportedAppProvider = SupportedAppProvider.getInstance(); + adapter = new AppAdapter(); + gridView = (GridView) view.findViewById(R.id.gridview); + gridView.setAdapter(adapter); + supportedAppProvider.populateSupportedAppsList(packageManager, activity, adapter); + } + + /** The adapter for the grid view that provides the view of each of the grid items. */ + private class AppAdapter extends BaseAdapter { + + @Override + public int getCount() { + return supportedAppProvider.getInstalledApps().size(); + } + + @Override + @Nullable + public Object getItem(int position) { + return null; + } + + @Override + public long getItemId(int position) { + return 0; + } + + private void updateAppItemFromBasicAppInfo(View appItemView, BasicAppInfo appInfo) { + TextView appNameView = (TextView) appItemView.findViewById(R.id.app_name); + appNameView.setText(appInfo.getAppName()); + ImageView iconView = (ImageView) appItemView.findViewById(R.id.app_icon); + iconView.setImageDrawable(appInfo.getIcon()); + + appItemView.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View view) { + try { + activity.startActivity(appInfo.getStartIntent()); + } catch (ActivityNotFoundException e) { + Toast.makeText(activity, R.string.error_opening_app, Toast.LENGTH_SHORT).show(); + Log.e(LOG_TAG, "Unable to launch activity " + appInfo.getPackageName() + ": " + e); + } + } + }); + } + + private int getIconPaddingColor(Palette palette, int defaultColor) { + return palette.getLightMutedColor( + palette.getMutedColor( + palette.getLightVibrantColor(palette.getVibrantColor(defaultColor)))); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = activity.getLayoutInflater().inflate(R.layout.app_grid_card, parent, false); + } + List installedApps = supportedAppProvider.getInstalledApps(); + updateAppItemFromBasicAppInfo(convertView, installedApps.get(position)); + ImageView iconView = (ImageView) convertView.findViewById(R.id.app_icon); + int color = + getIconPaddingColor( + installedApps.get(position).getPalette(), + ContextCompat.getColor(activity, R.color.transparent)); + + // Special case the "Cardboard Demos" icon padding color + if (installedApps.get(position).getPackageName().equals(activity.getPackageName())) { + // Light grey color surrounding the "Cardboard Demos" icon. Spec is here: + // https://folio.googleplex.com/ericamorse/Cardboard/November-release/spec#%2Fmy-library-spec.png + color = DEMO_ICON_PADDING_COLOR; + } + + iconView.setBackgroundColor(color); + return convertView; + } + } +} diff --git a/paperscope/core/SupportedAppProvider.java b/paperscope/core/SupportedAppProvider.java new file mode 100644 index 00000000..2e6d1b04 --- /dev/null +++ b/paperscope/core/SupportedAppProvider.java @@ -0,0 +1,161 @@ +package com.google.vr.cardboard.paperscope.demo; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.graphics.drawable.Drawable; +import android.util.Log; +import android.widget.BaseAdapter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * This class provides a list of supported apps to be displayed by the {@link MyLibraryFragment} + * fragment. Apps can advertise cardboard support by adding the following category section to a + * given intent filter in their AndroidManifest.xml: + */ +final class SupportedAppProvider { + private static SupportedAppProvider instance; + private static final String LOG_TAG = SupportedAppProvider.class.getName(); + private static final String CARDBOARD_SUPPORT_CATEGORY_INTENT_TAG = + "com.google.intent.category.CARDBOARD"; + private List installedApps; + + private SupportedAppProvider() { + installedApps = new ArrayList(); + } + + public static SupportedAppProvider getInstance() { + if (instance == null) { + instance = new SupportedAppProvider(); + } + return instance; + } + + @SuppressWarnings("deprecation") // This is ignored because our app minSdkVersion is 16. + private BasicAppInfo buildCardboardDemoAppInfo(Context context) { + Intent intent = new Intent(context, CardboardLauncherActivity.class); + + String packageName = context.getPackageName(); + BasicAppInfo appInfo = + new BasicAppInfo( + packageName, + context.getResources().getString(R.string.bundled_demos_label), + intent, + context.getResources().getDrawable(R.drawable.cardboard_demo)); + + return appInfo; + } + + public List getInstalledApps() { + return installedApps; + } + + private void sortAppListByName(List apps) { + Comparator comparator = + new Comparator() { + @Override + public int compare(BasicAppInfo first, BasicAppInfo second) { + return first.getAppName().compareTo(second.getAppName()); + } + }; + Collections.sort(apps, comparator); + } + + private List getCardboardActivities(PackageManager packageManager) { + Intent cardboardIntent = new Intent(Intent.ACTION_MAIN); + cardboardIntent.addCategory(CARDBOARD_SUPPORT_CATEGORY_INTENT_TAG); + return packageManager.queryIntentActivities(cardboardIntent, 0); + } + + private BasicAppInfo createBasicAppInfoFromResolveInfoAndIntent( + PackageManager pm, ResolveInfo resolveInfo, Intent intent) { + final String packageName = resolveInfo.activityInfo.packageName; + if (packageName == null) { + Log.w(LOG_TAG, "packageName not set in activityInfo"); + return null; + } + + CharSequence appName = resolveInfo.loadLabel(pm); + if (appName == null) { + Log.w(LOG_TAG, "application label not found for " + packageName); + return null; + } + + Drawable icon = pm.getApplicationIcon(resolveInfo.activityInfo.applicationInfo); + if (icon == null) { + Log.w(LOG_TAG, "application icon not found for " + packageName); + return null; + } + + return new BasicAppInfo(packageName, appName.toString(), intent, icon); + } + + private BasicAppInfo createBasicAppInfoFromResolveInfo( + PackageManager pm, ResolveInfo resolveInfo) { + // Create an intent that will launch the input activity. + final ActivityInfo activityInfo = resolveInfo.activityInfo; + Intent intent = new Intent(); + intent.setClassName(activityInfo.applicationInfo.packageName, activityInfo.name); + + return createBasicAppInfoFromResolveInfoAndIntent(pm, resolveInfo, intent); + } + + private void appendInstalledAppsWithCardboardSupportIntent( + PackageManager packageManager, Context context, Map supportedAppsMap) { + List cardboardApps = getCardboardActivities(packageManager); + for (ResolveInfo resolveInfo : cardboardApps) { + + // Check that the package name is not already in the list, if it was most likely we already + // added the intent for an activity in the same package that matched better. Therefore we are + // skipping the next one. + // This also avoid somebody abusing this "API" by declaring multiple activities from the same + // app. + if (supportedAppsMap.containsKey(resolveInfo.activityInfo.applicationInfo.packageName)) { + continue; + } + + // Check to make sure the activity is not in our own APK. + if (resolveInfo.activityInfo.applicationInfo.packageName.equalsIgnoreCase( + context.getPackageName())) { + continue; + } + + // Bundle the app name with its icon and start intent. + BasicAppInfo basicAppInfo = createBasicAppInfoFromResolveInfo(packageManager, resolveInfo); + + // Add the app info if valid to the list of apps. + if (basicAppInfo != null) { + supportedAppsMap.put(basicAppInfo.getPackageName(), basicAppInfo); + } + } + } + + private List getSupportedAppList(PackageManager packageManager, Context context) { + // Map of package name and basicAppInfo that will be used to ensure uniqueness. + Map supportedAppsMap = new LinkedHashMap(); + // Add apps that have cardboard intent. + appendInstalledAppsWithCardboardSupportIntent(packageManager, context, supportedAppsMap); + + List supportedApps = new ArrayList(supportedAppsMap.values()); + sortAppListByName(supportedApps); + return supportedApps; + } + + public void populateSupportedAppsList( + PackageManager packageManager, Context context, BaseAdapter adapter) { + installedApps.clear(); + installedApps.add(buildCardboardDemoAppInfo(context)); + installedApps.addAll(getSupportedAppList(packageManager, context)); + + adapter.notifyDataSetChanged(); + } +} diff --git a/paperscope/core/TransitionView.java b/paperscope/core/TransitionView.java new file mode 100644 index 00000000..de006dec --- /dev/null +++ b/paperscope/core/TransitionView.java @@ -0,0 +1,205 @@ +package com.google.vr.cardboard.paperscope.demo; + +import android.content.Context; +import android.graphics.Point; +import android.graphics.drawable.ColorDrawable; +import android.view.Display; +import android.view.LayoutInflater; +import android.view.OrientationEventListener; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; +import android.widget.FrameLayout; +import android.widget.ImageButton; +import android.widget.ImageView; + +/** The transition view, which is displayed before entering VR mode. */ +public class TransitionView extends FrameLayout { + + /** Interface for listening to events from the transition view. */ + public interface Listener { + public void onSwitchButtonClicked(); + + public void onExitButtonClicked(); + } + + public static final int TRANSITION_BACKGROUND_COLOR = 0xFF455A64; + + /** + * Duration (in milliseconds), of the animation used to dismiss the view after landspace left + * detected. + */ + public static final int TRANSITION_ANIMATION_DURATION_MS = 500; + + /** + * Amount of time (in milliseconds) to delay the dismissal of the transition view, if the device + * is initially in landscape left, so the user can still see the transition briefly before + * entering VR. + */ + public static final int ALREADY_LANDSCAPE_LEFT_TRANSITION_DELAY_MS = 2000; + + /** + * Tolerance (in degrees) for landscape device orientation detection. This should be a relatively + * low tolerance as it is used for auto-dismissal of this view when in landscape. If this is too + * high, then the transition view may dismiss before the phone is in the viewer. + */ + private static final int LANDSCAPE_TOLERANCE_DEGREES = 5; + + private final Context context; + private OrientationEventListener orientationEventListener; + private Listener transitionViewListener; + + private static boolean isLandscapeLeft(int orientationDegrees) { + return Math.abs(orientationDegrees - 270) < LANDSCAPE_TOLERANCE_DEGREES; + } + + public TransitionView(Context context, Listener listener) { + super(context); + + this.context = context; + transitionViewListener = listener; + setBackground(new ColorDrawable(TRANSITION_BACKGROUND_COLOR)); + inflateContentView(R.layout.transition_view); + // Rotate the view -90 degrees to simulate portrait + setRotation(-90); + } + + private void inflateContentView(int layoutId) { + removeAllViews(); + + LayoutInflater.from(getContext()).inflate(layoutId, this, true); + View switchActionView = findViewById(R.id.transition_switch_action); + switchActionView.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + // When the switch action is tapped, launched directly in to the change viewer + transitionViewListener.onSwitchButtonClicked(); + } + }); + + ImageButton backButton = (ImageButton) findViewById(R.id.back_button); + backButton.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View v) { + transitionViewListener.onExitButtonClicked(); + } + }); + + // Dismiss the view if the user clicks on the transition icon. + ImageView transitionIcon = (ImageView) findViewById(R.id.transition_icon); + transitionIcon.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + fadeOutAndRemove(/* delay= */ false); + } + }); + } + + @SuppressWarnings("deprecation") // This is ignored because our app minSdkVersion is 16. + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + // Get screen dimensions + Display display = + ((WindowManager) this.context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); + Point size = new Point(); + display.getSize(size); + // Landscape width = real screen width, height = screen height + int width = size.x; + display.getRealSize(size); + int height = size.y; + + // After rotation, we want the "portrait" layout: + ViewGroup.LayoutParams params = getLayoutParams(); + if (params == null) { + params = new ViewGroup.LayoutParams(height, width); + } else { + params.width = height; + params.height = width; + } + setLayoutParams(params); + + // Move it back into position (translate after rotation) + setTranslationX((width - height) / 2f); + setTranslationY((height - width) / 2f); + + startOrientationMonitor(); + } + + private void fadeOutAndRemove(boolean delay) { + // Once we are ready to fade out, immediately stop and invalidate the orientation listener. + stopOrientationMonitor(); + + Animation existingFadeOut = getAnimation(); + if (existingFadeOut != null) { + // Don't reschedule an animation if an existing one is running or will start before the new + // one. + if (delay || existingFadeOut.getStartOffset() == 0) { + return; + } + // The clearAnimation() call will trigger the listener's onAnimationEnd method, which + // will in turn detach the TransitionView. Avoid this by first clearing the listener. + existingFadeOut.setAnimationListener(null); + clearAnimation(); + } + + Animation newFadeOut = new AlphaAnimation(/* fromAlpha= */ 1, /* toAlpha= */ 0); + newFadeOut.setInterpolator(new AccelerateInterpolator()); + newFadeOut.setRepeatCount(0); + newFadeOut.setDuration(TRANSITION_ANIMATION_DURATION_MS); + if (delay) { + newFadeOut.setStartOffset(ALREADY_LANDSCAPE_LEFT_TRANSITION_DELAY_MS); + } + + newFadeOut.setAnimationListener( + new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) {} + + @Override + public void onAnimationEnd(Animation animation) { + TransitionView.this.setVisibility(View.GONE); + ((ViewGroup) getParent()).removeView(TransitionView.this); + } + + @Override + public void onAnimationRepeat(Animation animation) {} + }); + + startAnimation(newFadeOut); + } + + private void startOrientationMonitor() { + if (orientationEventListener != null) { + return; + } + + orientationEventListener = + new OrientationEventListener(getContext()) { + @Override + public void onOrientationChanged(int orientation) { + if (isLandscapeLeft(orientation)) { + fadeOutAndRemove(/* delay= */ false); + } + } + }; + + orientationEventListener.enable(); + } + + private void stopOrientationMonitor() { + if (orientationEventListener == null) { + return; + } + + orientationEventListener.disable(); + orientationEventListener = null; + } +} diff --git a/paperscope/magicwindow/AndroidManifest.xml b/paperscope/magicwindow/AndroidManifest.xml new file mode 100644 index 00000000..0f76e5f8 --- /dev/null +++ b/paperscope/magicwindow/AndroidManifest.xml @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/paperscope/magicwindow/MagicWindowActivity.java b/paperscope/magicwindow/MagicWindowActivity.java new file mode 100644 index 00000000..686ecbdd --- /dev/null +++ b/paperscope/magicwindow/MagicWindowActivity.java @@ -0,0 +1,257 @@ +package com.google.vr.cardboard.paperscope.demo.magicwindow; + +import com.google.vr.cardboard.paperscope.demo.R; + +import android.Manifest; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.res.AssetManager; +import android.net.Uri; +import android.opengl.GLSurfaceView; +import android.os.Build; +import android.os.Bundle; +import androidx.appcompat.app.AppCompatActivity; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewGroup.MarginLayoutParams; +import android.widget.TextView; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.google.android.material.snackbar.Snackbar; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +/** + * This is the main Activity for the demo application. It shows a monoscopic view of a VR world that + * is controlled by the user's head tracking. + */ +public class MagicWindowActivity extends AppCompatActivity { + static { + System.loadLibrary("cardboard_jni"); + } + + // Random request code for photos and videos. + private static final int PHOTOS_AND_VIDEOS_REQUEST_CODE = 234; + private static final String TAG = MagicWindowActivity.class.getSimpleName(); + + private static final Uri CARDBOARD_MANUFACTURERS_URL = + Uri.parse("https://developers.google.com/cardboard/manufacturers"); + + private long nativeApp; + private GLSurfaceView glView; + private Snackbar permissionRationalSnackbar; + private boolean alreadyAskedStoragePermission = false; + + @Override + public void onCreate(Bundle savedInstance) { + super.onCreate(savedInstance); + + nativeApp = nativeOnCreate(getAssets()); + + setContentView(R.layout.magic_window); + + View container = findViewById(R.id.coordinator); + ViewCompat.setOnApplyWindowInsetsListener( + container, + (v, windowInsets) -> { + Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()); + + MarginLayoutParams mlp = (MarginLayoutParams) v.getLayoutParams(); + mlp.leftMargin = insets.left; + mlp.bottomMargin = insets.bottom; + mlp.rightMargin = insets.right; + v.setLayoutParams(mlp); + + return WindowInsetsCompat.CONSUMED; + }); + + glView = findViewById(R.id.surface_view); + glView.setEGLContextClientVersion(2); + Renderer renderer = new Renderer(); + glView.setRenderer(renderer); + glView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY); + + TextView getOneTv = findViewById(R.id.get_cardboard); + getOneTv.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(Intent.ACTION_VIEW, CARDBOARD_MANUFACTURERS_URL); + startActivity(intent); + } + }); + + FloatingActionButton nextFab = findViewById(R.id.next); + nextFab.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + if (nativeHasSavedDeviceParams(nativeApp)) { + finish(); + } else { + nativeScanViewer(nativeApp); + } + } + }); + } + + @Override + protected void onPause() { + super.onPause(); + + nativeOnPause(nativeApp); + glView.onPause(); + } + + @Override + protected void onResume() { + super.onResume(); + + if (nativeHasSavedDeviceParams(nativeApp)) { + finish(); + } + + if (!hasMediaPermissions()) { + requestMediaPermissions(); + } + + glView.onResume(); + nativeOnResume(nativeApp); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + nativeOnDestroy(nativeApp); + nativeApp = 0; + } + + /** + * Gets whether UI should be shown with rationale before requesting a permission. + * + * @param permissions Permissions to request. + * @return True if UI with rationale should be shown before requesting a permission. Otherwise, + * return false. + */ + private boolean shouldShowRequestPermissionRationale(List permissions) { + for (String permission : permissions) { + if (ActivityCompat.shouldShowRequestPermissionRationale(this, permission)) { + return true; + } + } + return false; + } + + /** + * Gets the permissions required to access images and videos from the external storage. + * + *

Based on the API level, the permission to be requested may be different. When the API level + * is below Android T READ_EXTERNAL_STORAGE have to be requested. From Android T on, due to + * granular media permissions, READ_MEDIA_IMAGES and READ_MEDIA_VIDEO have to be requested. + * + * @return string array with permissions to be requested. + */ + private List getStoragePermissionsToRequest() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + return Collections.unmodifiableList( + Arrays.asList( + Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.READ_MEDIA_VIDEO)); + } + + return Collections.unmodifiableList(Arrays.asList(Manifest.permission.READ_EXTERNAL_STORAGE)); + } + + private void requestMediaPermissions() { + List permissions = getStoragePermissionsToRequest(); + if (shouldShowRequestPermissionRationale(permissions)) { + if (permissionRationalSnackbar == null) { + final View rootView = ((ViewGroup) findViewById(android.R.id.content)).getChildAt(0); + + permissionRationalSnackbar = + Snackbar.make( + rootView, R.string.read_media_permission_rationale, Snackbar.LENGTH_INDEFINITE) + .setAction( + android.R.string.ok, + new View.OnClickListener() { + @Override + public void onClick(View view) { + ActivityCompat.requestPermissions( + MagicWindowActivity.this, + permissions.toArray(new String[0]), + PHOTOS_AND_VIDEOS_REQUEST_CODE); + // After click, the current Snackbar will be automatically removed from the + // SnackbarManager so next time calling show() on it won't show it. As a + // result, + // we explicitly set it to be null so next time we will first create a new + // Snackbar then call show() on the new Snackbar. + permissionRationalSnackbar = null; + } + }); + } + + // Only call show() if the current Snackbar is not shown to avoid the animation. + if (!permissionRationalSnackbar.isShown()) { + permissionRationalSnackbar.show(); + } + } else if (!alreadyAskedStoragePermission) { + alreadyAskedStoragePermission = true; + // Storage permission has not been granted yet. Request it directly. + ActivityCompat.requestPermissions( + this, permissions.toArray(new String[0]), PHOTOS_AND_VIDEOS_REQUEST_CODE); + } + } + + private boolean hasMediaPermissions() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + return ContextCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_IMAGES) + == PackageManager.PERMISSION_GRANTED + && ContextCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_VIDEO) + == PackageManager.PERMISSION_GRANTED; + } else { + return ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) + == PackageManager.PERMISSION_GRANTED; + } + } + + private class Renderer implements GLSurfaceView.Renderer { + @Override + public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) { + nativeOnSurfaceCreated(nativeApp); + } + + @Override + public void onSurfaceChanged(GL10 gl10, int width, int height) { + nativeSetScreenParams(nativeApp, width, height); + } + + @Override + public void onDrawFrame(GL10 gl10) { + nativeOnDrawFrame(nativeApp); + } + } + + private native long nativeOnCreate(AssetManager assetManager); + + private native void nativeOnDestroy(long nativeApp); + + private native void nativeOnResume(long nativeApp); + + private native void nativeOnPause(long nativeApp); + + private native void nativeOnSurfaceCreated(long nativeApp); + + private native void nativeSetScreenParams(long nativeApp, int width, int height); + + private native void nativeOnDrawFrame(long nativeApp); + + private native void nativeScanViewer(long nativeApp); + + private native boolean nativeHasSavedDeviceParams(long nativeApp); +} diff --git a/paperscope/myvideos/AndroidManifest.xml b/paperscope/myvideos/AndroidManifest.xml new file mode 100644 index 00000000..dcda03b7 --- /dev/null +++ b/paperscope/myvideos/AndroidManifest.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/paperscope/myvideos/ImageFetcher.java b/paperscope/myvideos/ImageFetcher.java new file mode 100644 index 00000000..f3ae82a1 --- /dev/null +++ b/paperscope/myvideos/ImageFetcher.java @@ -0,0 +1,136 @@ +package com.google.vr.cardboard.paperscope.demo.myvideos; + +import android.content.ContentResolver; +import android.content.Context; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Handler; +import android.os.Message; +import android.util.Pair; +import android.util.Size; +import com.google.cardboard.sdk.CardboardView; +import com.google.common.flogger.FluentLogger; +import com.google.vr.cardboard.paperscope.demo.myvideos.VideoFetcher.VideoInfo; +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Simple image downloader in a background thread. + */ +public class ImageFetcher { + + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + private static final int DISPLAY_DELAY_MILLIS = 50; + private static final int THUMBNAIL_WIDTH_SIZE = 512; + private static final int THUMBNAIL_HEIGHT_SIZE = 384; + + private final CardboardView cardboardView; + private final MyVideosRenderer renderer; + private final Handler handler; + private int fetchCount; + private ExecutorService executor = Executors.newSingleThreadExecutor(); + + private final ContentResolver contentResolver; + + public ImageFetcher( + Context context, final CardboardView cardboardView, final MyVideosRenderer renderer) { + this.cardboardView = cardboardView; + this.renderer = renderer; + contentResolver = context.getContentResolver(); + handler = new ImageHandler(this); + } + + @SuppressWarnings("deprecation") // This is ignored because our app minSdkVersion is 16. + private static class ImageHandler extends Handler { + private final WeakReference imageFetcherReference; + + ImageHandler(ImageFetcher imageFetcher) { + imageFetcherReference = new WeakReference<>(imageFetcher); + } + + @Override + public void handleMessage(Message msg) { + ImageFetcher imageFetcher = imageFetcherReference.get(); + if (imageFetcher == null) { + return; + } + + if (msg.obj instanceof Bitmap) { + final Bitmap bitmap = (Bitmap) msg.obj; + final int index = msg.what; + // Delay the loading of each thumbnail by a bit so that all the GL textures don't get + // created at the same time, causing stuttering. + // TODO(kvaalen): it's still possible for multiple thumbnails to be added at the same + // time + // since these times are relative to when the thumbnail loaded and are not + // absolute. + postDelayed( + () -> { + // Use queueEvent to have it run on the GL thread. + imageFetcher.cardboardView.queueEvent( + () -> imageFetcher.renderer.addThumbnail(bitmap, index)); + }, + imageFetcher.fetchCount * ((long) DISPLAY_DELAY_MILLIS)); + imageFetcher.fetchCount++; + } else { + logger.atSevere().log("Object isn't a bitmap"); + } + } + } + + /** + * @param indexAndPaths Pairs of file path strings and their index in the list of videos loaded. + */ + public void downloadImages(final List> indexAndVideosInfo) { + Runnable runnable = + () -> { + for (Pair indexAndVideoInfo : indexAndVideosInfo) { + if (Thread.currentThread().isInterrupted()) { + return; + } + doDownload(indexAndVideoInfo.first, indexAndVideoInfo.second); + } + }; + executor.execute(runnable); + } + + /** + * Creates the bitmap for the thumbnail for the video loaded. Once the bitmap is created, it is + * queued for rendering by sending it to the handler. + * + *

Based on the API level, the bitmap is created from different sources. When the API level is + * below Android Q´s the bitmap is created using the absolute path of the video. From Android Q, + * the bitmap is created using the Uri of the video. + * + * @param index Index of the video file in the loaded videos list. + * @param videoInfo Structure containing the file path string and the URI of the loaded video. + */ + private void doDownload(int index, VideoInfo videoInfo) { + Bitmap bitmap = null; + Uri videoUri = videoInfo.getVideoStreamUri(); + Size thumbnailSize = new Size(THUMBNAIL_WIDTH_SIZE, THUMBNAIL_HEIGHT_SIZE); + try { + bitmap = contentResolver.loadThumbnail(videoUri, thumbnailSize, null); + } catch (IOException e) { + logger.atSevere().log( + "Error getting bitmap from Uri %s : %s", videoUri.getPath(), e.getMessage()); + } + + if (!Thread.currentThread().isInterrupted()) { + handler.obtainMessage(index, bitmap).sendToTarget(); + } + } + + public void reset() { + // Note: This also resets the thumbnail loading delay in the handler. + fetchCount = 0; + executor.shutdownNow(); + executor = Executors.newSingleThreadExecutor(); + // Cancel all previous thumbnail update messages in the handler, since they are now obsolete. + handler.removeCallbacksAndMessages(null); + } + +} diff --git a/paperscope/myvideos/MyVideosActivity.java b/paperscope/myvideos/MyVideosActivity.java new file mode 100644 index 00000000..34adeb9d --- /dev/null +++ b/paperscope/myvideos/MyVideosActivity.java @@ -0,0 +1,12 @@ +package com.google.vr.cardboard.paperscope.demo.myvideos; + +import com.google.vr.cardboard.paperscope.demo.common.DemoBaseActivity; +import com.google.vr.cardboard.paperscope.demo.common.DemoBaseController; + +/** Demo that allows the user to view videos stored on their device. */ +public class MyVideosActivity extends DemoBaseActivity { + @Override + protected DemoBaseController createController() { + return new MyVideosController(cardboardView); + } +} diff --git a/paperscope/myvideos/MyVideosController.java b/paperscope/myvideos/MyVideosController.java new file mode 100644 index 00000000..80e58dea --- /dev/null +++ b/paperscope/myvideos/MyVideosController.java @@ -0,0 +1,48 @@ +package com.google.vr.cardboard.paperscope.demo.myvideos; + +import com.google.cardboard.sdk.CardboardView; +import com.google.vr.cardboard.paperscope.demo.common.DemoBaseController; + +/** Demo that allows the user to view My Videos. */ +public class MyVideosController extends DemoBaseController { + private final VideoSequencer sequencer; + + public MyVideosController(CardboardView cardboardView) { + super(cardboardView); + renderer = new MyVideosRenderer(cardboardView.getContext()); + sequencer = new VideoSequencer(cardboardView.getContext(), cardboardView, renderer); + this.cardboardView.setRenderer(renderer); + } + + @Override + public void onResume() { + super.onResume(); + sequencer.startPlayback(); + } + + @Override + public void onPause() { + super.onPause(); + sequencer.stop(); + } + + @Override + public void shutdown() { + super.shutdown(); + sequencer.release(); + } + + @Override + public void enableOnTriggerEventListener() { + cardboardView.setOnTriggerEvent( + () -> { + if (renderer != null && sequencer != null) { + if (renderer.isScreenOnlyTargeted()) { + sequencer.togglePause(); + } else { + sequencer.playTargetedVideo(); + } + } + }); + } +} diff --git a/paperscope/myvideos/MyVideosPlayer.java b/paperscope/myvideos/MyVideosPlayer.java new file mode 100644 index 00000000..13ff1b57 --- /dev/null +++ b/paperscope/myvideos/MyVideosPlayer.java @@ -0,0 +1,291 @@ +package com.google.vr.cardboard.paperscope.demo.myvideos; + +import android.content.Context; +import android.graphics.SurfaceTexture; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.net.Uri; +import android.os.Handler; +import android.view.Surface; +import com.google.common.flogger.FluentLogger; +import com.google.vr.cardboard.paperscope.demo.myvideos.VideoFetcher.VideoInfo; + +/** Playback controller for local videos. */ +public class MyVideosPlayer implements MyVideosRenderer.SurfaceTextureListener { + + /** Simple listener interface for video playback events. */ + public interface Listener { + void onVideoEnded(); + void onPlaybackError(int what, int extra); + void onPlaybackSucceeded(); + } + + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + private final Context context; + private final Listener listener; + private final MyVideosRenderer renderer; + private final AudioManager audioManager; + private final ProgressTracker progressTracker; + + private MediaPlayer mediaPlayer; + private SurfaceTexture surfaceTexture; + private boolean isSurfaceCreated; + private Uri pendingStreamUri; + private boolean isPaused; + + public MyVideosPlayer(Context context, Listener listener, MyVideosRenderer renderer) { + this.context = context; + this.listener = listener; + this.renderer = renderer; + audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + progressTracker = new ProgressTracker(); + } + + @SuppressWarnings("deprecation") // This is ignored because our app minSdkVersion is 16. + public void stop() { + progressTracker.stopTracking(); + // Release the player instead of stopping so that an async prepare gets stopped. + releasePlayer(); + audioManager.abandonAudioFocus(null); + } + + public boolean isPaused() { + return isPaused; + } + + public void togglePause() { + if (mediaPlayer != null) { + if (isPaused) { + mediaPlayer.start(); + } else { + mediaPlayer.pause(); + } + isPaused = !isPaused; + } + } + + public void play() { + if (mediaPlayer != null && isPaused) { + togglePause(); + } + } + + public void pause() { + if (mediaPlayer != null && !isPaused) { + togglePause(); + } + } + + public void play(VideoInfo video) { + startPlaybackStream(video.getVideoStreamUri()); + } + + public void release() { + progressTracker.stopTracking(); + releasePlayer(); + } + + public void releasePlayer() { + if (mediaPlayer != null) { + isPaused = false; + mediaPlayer.release(); + mediaPlayer = null; + } + } + + private void startPlaybackStream(Uri streamUri) { + renderer.setIsPlaybackStarting(true); + renderer.setIsBuffering(true); + if (!isSurfaceCreated) { + pendingStreamUri = streamUri; + return; + } + + pendingStreamUri = null; + + if (mediaPlayer != null) { + releasePlayer(); + } + mediaPlayer = createMediaPlayer(); + + try { + mediaPlayer.setDataSource(context, streamUri); + } catch (Exception e) { + logger.atSevere().log( + "MediaPlayer setMediaDataSource exception with stream: %s %s", streamUri, e.getMessage()); + return; + } + + // Prepare and start the method asynchronously, otherwise it blocks the main thread. + mediaPlayer.setOnPreparedListener( + new MediaPlayer.OnPreparedListener() { + @Override + public void onPrepared(MediaPlayer mediaPlayer) { + if (mediaPlayer.getVideoHeight() == 0) { + logger.atSevere().log("Video looks invalid, its height is zero."); + return; + } + + float videoAspectRatio = + ((float) mediaPlayer.getVideoWidth()) / mediaPlayer.getVideoHeight(); + renderer.setVideoAspectRatio(videoAspectRatio); + mediaPlayer.start(); + } + }); + + try { + mediaPlayer.prepareAsync(); + progressTracker.startTracking(); + } catch (IllegalStateException e) { + logger.atSevere().log("MediaPlayer prepare() failed %s", e.getMessage()); + return; + } + } + + @SuppressWarnings("deprecation") // This is ignored because our app minSdkVersion is 16. + private MediaPlayer createMediaPlayer() { + MediaPlayer mediaPlayer = new MediaPlayer(); + mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { + @Override + public void onCompletion(MediaPlayer mediaPlayer) { + listener.onVideoEnded(); + } + }); + mediaPlayer.setOnErrorListener( + new MediaPlayer.OnErrorListener() { + @Override + public boolean onError(MediaPlayer mediaPlayer, int what, int extra) { + logger.atSevere().log("MediaPlayer error - what: %s, extra: %s", what, extra); + // TODO(kvaalen): Have more fine grained control over the listener events. + listener.onPlaybackError(what, extra); + return true; + } + }); + mediaPlayer.setOnInfoListener( + new MediaPlayer.OnInfoListener() { + @Override + public boolean onInfo(MediaPlayer mediaPlayer, int what, int extra) { + switch (what) { + case MediaPlayer.MEDIA_INFO_BUFFERING_START -> renderer.setIsBuffering(true); + case MediaPlayer.MEDIA_INFO_BUFFERING_END -> { + renderer.setIsBuffering(false); + listener.onPlaybackSucceeded(); + } + case MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START -> { + renderer.setIsPlaybackStarting(false); + renderer.setIsBuffering(false); + } + } + return true; + } + }); + + Surface surface = new Surface(surfaceTexture); + mediaPlayer.setSurface(surface); + surface.release(); + + return mediaPlayer; + } + + // MyVideosStereoRenderer.SurfaceTextureListener + + @Override + public void onSurfaceTextureCreated(SurfaceTexture surfaceTexture) { + isSurfaceCreated = true; + this.surfaceTexture = surfaceTexture; + if (pendingStreamUri != null) { + startPlaybackStream(pendingStreamUri); + } + } + + /** + * Sends pings to check the progress of the video every so often. + */ + private class ProgressTracker { + + private static final int DELAY_MILLIS = 100; + + private final Handler handler; + private final Runnable ping; + + private boolean lastIsBuffering; + private boolean lastIsPlaybackStarting; + + @SuppressWarnings("deprecation") // This is ignored because our app minSdkVersion is 16. + public ProgressTracker() { + handler = new Handler(); + ping = new Runnable() { + @Override + public void run() { + ping(); + } + }; + } + + public void startTracking() { + handler.removeCallbacksAndMessages(null); + lastIsBuffering = true; + lastIsPlaybackStarting = true; + ping(); + } + + public void stopTracking() { + handler.removeCallbacksAndMessages(null); + } + + private void ping() { + checkIsPlaybackStarted(); + checkBufferingStatus(); + float ratio; + if (mediaPlayer != null && mediaPlayer.isPlaying()) { + int pos = mediaPlayer.getCurrentPosition(); + int duration = mediaPlayer.getDuration(); + ratio = pos / (float) duration; + } else { + ratio = 0f; + } + renderer.setProgressRatio(ratio); + handler.postDelayed(ping, DELAY_MILLIS); + } + + /** + * For some reason the Galaxy Note 2 doesn't get the onInfo events from the MediaPlayer. + * This check is a fallback to detect if a buffering spinner should be shown. + * TODO(kvaalen): Figure out why the Galaxy Note 2 doesn't get onInfo events and remove this + * when it's fixed. + */ + private void checkBufferingStatus() { + boolean isBuffering = mediaPlayer == null || !mediaPlayer.isPlaying(); + if (isBuffering == lastIsBuffering) { + return; + } + + renderer.setIsBuffering(isBuffering && !isPaused); + lastIsBuffering = isBuffering; + } + + /** + * For some reason the Galaxy Note 2 doesn't get the onInfo events from the MediaPlayer. + * This check is a fallback to detect if the splash screen should be shown. + * TODO(kvaalen): Figure out why the Galaxy Note 2 doesn't get onInfo events and remove this + * when it's fixed. + */ + private void checkIsPlaybackStarted() { + boolean isPlaybackStarting = + !isPaused + && (mediaPlayer == null + || !mediaPlayer.isPlaying() + || mediaPlayer.getCurrentPosition() == 0); + if (isPlaybackStarting == lastIsPlaybackStarting) { + return; + } + + renderer.setIsPlaybackStarting(isPlaybackStarting); + lastIsPlaybackStarting = isPlaybackStarting; + } + + } + +} diff --git a/paperscope/myvideos/MyVideosRenderer.java b/paperscope/myvideos/MyVideosRenderer.java new file mode 100644 index 00000000..9c8cebe3 --- /dev/null +++ b/paperscope/myvideos/MyVideosRenderer.java @@ -0,0 +1,1246 @@ +package com.google.vr.cardboard.paperscope.demo.myvideos; + +import com.google.vr.cardboard.paperscope.demo.R; + +import static java.lang.Math.max; +import static java.lang.Math.min; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.SurfaceTexture; +import android.opengl.GLES20; +import android.opengl.GLUtils; +import android.opengl.Matrix; +import android.os.SystemClock; +import com.google.cardboard.sdk.CardboardView; +import com.google.cardboard.sdk.CardboardView.Eye; +import com.google.cardboard.sdk.HeadTransform; +import com.google.cardboard.sdk.Viewport; +import com.google.common.base.VerifyException; +import com.google.common.flogger.FluentLogger; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import javax.microedition.khronos.egl.EGLConfig; + +/** Renderer that displays a main video screen and a grid of video thumbnails around the viewer. */ +public class MyVideosRenderer + implements CardboardView.Renderer, SurfaceTexture.OnFrameAvailableListener { + + /** Listener for surface texture created events . */ + public interface SurfaceTextureListener { + void onSurfaceTextureCreated(SurfaceTexture surfaceTexture); + } + + private volatile int activeThumbnail = -1; + private final float[] thumbnailDistances; + private final float[] thumbnailBrightness; + private final float[] thumbnailAlpha; + private volatile int nowPlayingIndex = -1; + + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + private static final int NUM_SPRITE_TEXTURES = 7; + private static final int NUM_THUMBNAIL_TEXTURES = 80; + + private static final int FLOAT_SIZE_BYTES = 4; + private static final int TRIANGLE_VERTICES_DATA_STRIDE_BYTES = 5 * FLOAT_SIZE_BYTES; + private static final int TRIANGLE_VERTICES_DATA_POS_OFFSET = 0; + private static final int TRIANGLE_VERTICES_DATA_UV_OFFSET = 3; + + // All distances in meters unless otherwise specified. + + private static final float SCENE_MULTIPLIER = 1; + private static final float CAMERA_DISTANCE = 5 * SCENE_MULTIPLIER; + + private static final int FBO_WIDTH = 8; + private static final int FBO_HEIGHT = 8; + private boolean enableFramebuffer = false; + private ByteBuffer framePixels; + + // Red, green and blue values for the glow. + private final float[] glowValues; + + // Distance from camera to thumbnail, thumbnail sphere radius. + private static final float DEFAULT_THUMBNAIL_DISTANCE = 4 * CAMERA_DISTANCE; + private static final float ACTIVE_THUMBNAIL_DISTANCE = 2.5f * CAMERA_DISTANCE; + private static final float Z_NEAR = 1; + private static final float Z_FAR = DEFAULT_THUMBNAIL_DISTANCE * 2 * SCENE_MULTIPLIER; + private static final float HALF_VIDEO_WIDTH = 2.8f * SCENE_MULTIPLIER; + private static final float FLOOR_TILE_SIZE = 4f * SCENE_MULTIPLIER; + private static final float VIDEO_WIDTH = HALF_VIDEO_WIDTH * 2; + + // The default video aspect ratio. This is used to display the video glow, which acts like a + // a TV frame. + private static final float DEFAULT_ASPECT_RATIO = (16 / 9f); + private static final float VIDEO_HEIGHT = VIDEO_WIDTH / DEFAULT_ASPECT_RATIO; + private static final float HALF_VIDEO_HEIGHT = VIDEO_HEIGHT / 2; + private static final float CAMERA_HEIGHT = 0; + // Angle between sphere slices/rows. + private static final float ROW_ANGLE = 18; + // Number of sphere rows. N translates to center row + N-1 rows on top and N-1 on bottom. + private static final int ROWS_COUNT = 3; + // Angle reserved for screen, thumbnails wouldn't be drawn within it. + private static final float SCREEN_ANGLE = 100f; + // Approximate screen angles used to tell if the user is targeting the screen when pulling the + // magnet. Don't target the side edges of the screen. + private static final float TARGET_SCREEN_YAW = SCREEN_ANGLE - 40f; + private static final float TARGET_SCREEN_PITCH_UP = 45f; + private static final float TARGET_SCREEN_PITCH_DOWN = -20f; + private static final float TARGET_SEARCH_YAW = SCREEN_ANGLE - 80f; + private static final float TARGET_SEARCH_PITCH_UP = -30f; + private static final float TARGET_SEARCH_PITCH_DOWN = -45f; + private static final float HALF_THUMBNAIL_WIDTH = 3.05f * SCENE_MULTIPLIER; + private static final float THUMBNAIL_WIDTH = HALF_THUMBNAIL_WIDTH * 2; + private static final float THUMBNAIL_HEIGHT = THUMBNAIL_WIDTH * (9 / 16f); + private static final float HALF_THUMBNAIL_HEIGHT = THUMBNAIL_HEIGHT / 2; + private static final float SEARCH_SIZE = HALF_THUMBNAIL_HEIGHT / 4f; + + private static final float[] videoVerticesData = { + // X, Y, Z, U, V + -HALF_VIDEO_WIDTH, HALF_VIDEO_HEIGHT, 0, 1, 1, HALF_VIDEO_WIDTH, HALF_VIDEO_HEIGHT, 0, 0, 1, + -HALF_VIDEO_WIDTH, -HALF_VIDEO_HEIGHT, 0, 1, 0, HALF_VIDEO_WIDTH, -HALF_VIDEO_HEIGHT, 0, 0, 0, + }; + + private static final float[] thumbnailVerticesData = { + // X, Y, Z, U, V + -HALF_THUMBNAIL_WIDTH, HALF_THUMBNAIL_HEIGHT, 0, 1, 1, HALF_THUMBNAIL_WIDTH, + HALF_THUMBNAIL_HEIGHT, 0, 0, 1, + -HALF_THUMBNAIL_WIDTH, -HALF_THUMBNAIL_HEIGHT, 0, 1, 0, HALF_THUMBNAIL_WIDTH, + -HALF_THUMBNAIL_HEIGHT, 0, 0, 0, + }; + + private static final float[] floorVerticesData = { + // X, Y, Z, U, V + -FLOOR_TILE_SIZE, FLOOR_TILE_SIZE, 0, 1, 1, FLOOR_TILE_SIZE, FLOOR_TILE_SIZE, 0, 0, 1, + -FLOOR_TILE_SIZE, -FLOOR_TILE_SIZE, 0, 1, 0, FLOOR_TILE_SIZE, -FLOOR_TILE_SIZE, 0, 0, 0, + }; + + private static final float[] lightboxVerticesData = { + // X,Y,Z, U, V + 1, 1, 0, 1, 1, -1, 1, 0, 0, 1, 1, -1, 0, 1, 0, -1, -1, 0, 0, 0, + }; + + private static final float[] searchVerticesData = { + // X,Y,Z, U, V + SEARCH_SIZE, + SEARCH_SIZE, + 0, + 0, + 1, + -SEARCH_SIZE, + SEARCH_SIZE, + 0, + 1, + 1, + SEARCH_SIZE, + -SEARCH_SIZE, + 0, + 0, + 0, + -SEARCH_SIZE, + -SEARCH_SIZE, + 0, + 1, + 0, + }; + + // The seek bar is a thin bar just under the video. It will get scaled in the X axis to be the + // right width. + private static final float[] seekBarVerticesData = { + // X, Y, Z, U, V + 0, + -HALF_VIDEO_HEIGHT * 1.01f, + 0, + 0, + 1, + -VIDEO_WIDTH, + -HALF_VIDEO_HEIGHT * 1.01f, + 0, + 1, + 1, + 0, + -HALF_VIDEO_HEIGHT * 1.02f, + 0, + 0, + 0, + -VIDEO_WIDTH, + -HALF_VIDEO_HEIGHT * 1.02f, + 0, + 1, + 0, + }; + + private static final float DEFAULT_THUMBNAIL_BRIGHTNESS = 0.5f; + private static final float ACTIVE_THUMBNAIL_BRIGHTNESS = 1.1f; + private static final float NOW_PLAYING_THUMBNAIL_BRIGHTNESS = 0.4f; + private static final float MIN_VIDEO_GLOW_BRIGHTNESS = 0.2f; + // The red, green, and blue brightness values for the glow when no video is playing. + // It has a red tint to match the red background when there is no video. + private static final float[] startVideoGlowBrightness = {1f, .2f, .5f}; + + // Full screen quad shader for drawing of a texture using texture coordinates as screen space + // vertex coordinates. + private static final String FSQ_VERTEX_SHADER = + """ + const vec2 xy_rescale = vec2(0.5, 0.5); + attribute vec4 aPosition; + varying vec2 vTextureCoord; + void main() { + vTextureCoord = xy_rescale + aPosition.xy * xy_rescale; + gl_Position = vec4(aPosition.xy, 0.0, 1.0); + }\ + """; + + private static final String FSQ_FRAGMENT_SHADER = + """ + #extension GL_OES_EGL_image_external : require + precision mediump float; + varying vec2 vTextureCoord; + uniform samplerExternalOES imageTexture; + void main() { + vec4 color = texture2D(imageTexture, vTextureCoord); + gl_FragColor = color + vec4(vTextureCoord.x * 0.1, vTextureCoord.y * 0.1, 0.0, 1.0); + } + """; + + private static final String VERTEX_SHADER = + """ + uniform mat4 uMvpMatrix; + uniform mat4 uStMatrix; + attribute vec4 aPosition; + attribute vec4 aTextureCoord; + varying vec2 vTextureCoord; + void main() { + gl_Position = uMvpMatrix * aPosition; + vTextureCoord = (uStMatrix * aTextureCoord).xy; + } + """; + + private static final String FRAGMENT_SHADER = + """ + precision mediump float; + varying vec2 vTextureCoord; + uniform sampler2D imageTexture; + uniform vec4 uBrightness; + void main() { + gl_FragColor = texture2D(imageTexture, vTextureCoord) * uBrightness; + } + """; + + // Fragment shader using the samplerExternalOES so that it can use video as a texture. + private static final String VIDEO_FRAGMENT_SHADER = + """ + #extension GL_OES_EGL_image_external : require + precision mediump float; + varying vec2 vTextureCoord; + uniform samplerExternalOES sTexture; + void main() { + gl_FragColor = texture2D(sTexture, vTextureCoord); + } + """; + + /** Struct holding a program and its handlers. */ + private static class ProgramHolder { + int program; + int uMvpMatrix; + int uStMatrix; + int aPosition; + int aTexture; + int uBrightness; + } + + private static class FsqProgramHolder { + int program; + int aPosition; + } + + private final FloatBuffer videoVertices; + private final FloatBuffer thumbnailVertices; + private final FloatBuffer lightboxVertices; + private final FloatBuffer seekBarVertices; + private final FloatBuffer floorVertices; + private final FloatBuffer searchVertices; + + private final float[] mvpMatrix = new float[16]; + private final float[] tmpMatrix = new float[16]; + private final float[] videoStMatrix = new float[16]; + private final float[] spriteStMatrix = new float[16]; + + // Model matrix + private final float[] modelMatrix = new float[16]; + // View matrix + private final float[] viewMatrix = new float[16]; + private final float[] lookAtMatrix = new float[16]; + private final float[] headViewMatrix = new float[16]; + + // We use a separate program for the video texture since it uses its own fragment shader. + private ProgramHolder videoProgramHolder; + // Program used for all image based textures. + private ProgramHolder spriteProgramHolder; + private FsqProgramHolder fsqProgramHolder; + + private int[] textureIds; + private int[] thumbnailTextureIds; + private int transitionBackgroundTextureId; + private int noVideoBackgroundTextureId; + private int videoGlowTextureId; + private int nowPlayingTextureId; + private int seekBarTextureId; + private int videoTextureId; + private int floorTextureId; + + private SurfaceTexture surfaceTexture; + private boolean surfaceNeedsUpdate = false; + private long initialTime; + private float animationTime; + + private static final int GL_TEXTURE_EXTERNAL_OES = 0x8D65; + + private final Context context; + private final Bitmap[] thumbnailBitmaps; + + private SurfaceTextureListener surfaceTextureListener; + + private boolean isPlaybackStarting; + private float seekBarRatio; + + private final float[] eulerAngles = new float[3]; + private float lastYaw = 0.0f; + private float lastPitch = 0.0f; + private float targetScreenYaw; + private float screenYaw; + private boolean isScreenTargeted; + private boolean isSearchTargeted; + + private int[] fboTargetTextureIds; + private int fboTargetTextureId; + private int framebuffer; + private int frameCounter = 0; + + private boolean isListening; + + // Whether there is no videos to display, in which case we show a "no video" background + // explaining to the user that they can take videos to view them here. + private boolean noVideos = true; + + // Video aspect ratio. Will be updated according to video played. + private float videoAspectRatio = DEFAULT_ASPECT_RATIO; + + public MyVideosRenderer(Context context) { + this.context = context; + + videoVertices = + ByteBuffer.allocateDirect(videoVerticesData.length * FLOAT_SIZE_BYTES) + .order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + videoVertices.put(videoVerticesData).position(0); + + thumbnailVertices = + ByteBuffer.allocateDirect(thumbnailVerticesData.length * FLOAT_SIZE_BYTES) + .order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + thumbnailVertices.put(thumbnailVerticesData).position(0); + + lightboxVertices = + ByteBuffer.allocateDirect(lightboxVerticesData.length * FLOAT_SIZE_BYTES) + .order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + lightboxVertices.put(lightboxVerticesData).position(0); + + seekBarVertices = + ByteBuffer.allocateDirect(seekBarVerticesData.length * FLOAT_SIZE_BYTES) + .order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + seekBarVertices.put(seekBarVerticesData).position(0); + + floorVertices = + ByteBuffer.allocateDirect(floorVerticesData.length * FLOAT_SIZE_BYTES) + .order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + floorVertices.put(floorVerticesData).position(0); + + searchVertices = + ByteBuffer.allocateDirect(searchVerticesData.length * FLOAT_SIZE_BYTES) + .order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + searchVertices.put(searchVerticesData).position(0); + + thumbnailBitmaps = new Bitmap[NUM_THUMBNAIL_TEXTURES]; + thumbnailDistances = new float[NUM_THUMBNAIL_TEXTURES]; + thumbnailBrightness = new float[NUM_THUMBNAIL_TEXTURES]; + thumbnailAlpha = new float[NUM_THUMBNAIL_TEXTURES]; + glowValues = new float[3]; + + isPlaybackStarting = true; + } + + public synchronized void setVideoAspectRatio(float videoAspectRatio) { + // We don't want to go over DEFAULT_ASPECT_RATIO otherwise the video would go over the video + // frame. + this.videoAspectRatio = min(DEFAULT_ASPECT_RATIO, videoAspectRatio); + } + + public synchronized float getVideoAspectRatio() { + return videoAspectRatio; + } + + public void setSurfaceTextureListener(SurfaceTextureListener surfaceTextureListener) { + this.surfaceTextureListener = surfaceTextureListener; + } + + public void setIsBuffering(boolean isBuffering) {} + + public void setIsPlaybackStarting(boolean isPlaybackStarting) { + this.isPlaybackStarting = isPlaybackStarting; + setGlowValues( + startVideoGlowBrightness[0], startVideoGlowBrightness[1], startVideoGlowBrightness[2]); + } + + private void setGlowValues(float r, float g, float b) { + glowValues[0] = r; + glowValues[1] = g; + glowValues[2] = b; + } + + public void setCurrentVideoIndex(int videoIndex) { + this.nowPlayingIndex = videoIndex; + } + + public void setProgressRatio(float seekBarRatio) { + this.seekBarRatio = seekBarRatio; + } + + public int getMaxThumbnailCount() { + return NUM_THUMBNAIL_TEXTURES; + } + + public synchronized int getActiveThumbnailId() { + return activeThumbnail; + } + + public boolean isScreenOnlyTargeted() { + return isScreenTargeted && activeThumbnail == -1; + } + + public boolean isSearchTargeted() { + return isSearchTargeted; + } + + public void centerScreen() { + targetScreenYaw = lastYaw + screenYaw - 180f; + } + + private synchronized void setNoVideos(boolean noVideos) { + this.noVideos = noVideos; + } + + private synchronized boolean hasNoVideos() { + return noVideos; + } + + public void addThumbnail(Bitmap bitmap, int index) { + if (index >= NUM_THUMBNAIL_TEXTURES) { + logger.atSevere().log("Adding thumbnail with an index too high"); + return; + } + + // All thumbnails at the quality we request have letterboxing. Crop it out. + int cropAmount = bitmap.getHeight() / 8; + Bitmap croppedBitmap = + Bitmap.createBitmap( + bitmap, 0, cropAmount, bitmap.getWidth(), bitmap.getHeight() - 2 * cropAmount); + // Textures sizes need to be a power of 2. + Bitmap scaledBitmap = Bitmap.createScaledBitmap(croppedBitmap, 256, 256, false); + thumbnailBitmaps[index] = scaledBitmap; + loadThumbnailTexture(index); + + bitmap.recycle(); + croppedBitmap.recycle(); + } + + public void onThumbnailSelected(int id) { + if (id < NUM_THUMBNAIL_TEXTURES && thumbnailBitmaps[id] != null && activeThumbnail != -1) { + // If the user selects a thumbnail, show that it is selected by making it pop out and zoom + // again. + thumbnailDistances[id] = DEFAULT_THUMBNAIL_DISTANCE; + } + } + + @Override + public void onNewFrame(HeadTransform headTransform) { + headTransform.getHeadView(headViewMatrix, 0); + // Yaw must be remapped due to sphere being upside down. + headTransform.getEulerAngles(eulerAngles, 0); + lastYaw = 180f - (float) Math.toDegrees(-eulerAngles[1]) - screenYaw; + lastYaw = lastYaw % 360f; + if (lastYaw < 0f) { + lastYaw += 360f; + } + lastPitch = (float) Math.toDegrees(eulerAngles[0]); + isScreenTargeted = + lastYaw > 180f - (TARGET_SCREEN_YAW / 2) + && lastYaw < 180f + (TARGET_SCREEN_YAW / 2) + && lastPitch < TARGET_SCREEN_PITCH_UP + && lastPitch > TARGET_SCREEN_PITCH_DOWN; + isSearchTargeted = + lastYaw > 180f - (TARGET_SEARCH_YAW / 2) + && lastYaw < 180f + (TARGET_SEARCH_YAW / 2) + && lastPitch < TARGET_SEARCH_PITCH_UP + && lastPitch > TARGET_SEARCH_PITCH_DOWN; + + synchronized (this) { + if (surfaceNeedsUpdate) { + surfaceTexture.updateTexImage(); + surfaceTexture.getTransformMatrix(videoStMatrix); + surfaceNeedsUpdate = false; + } + } + animationTime = SystemClock.uptimeMillis() - initialTime; + + if (enableFramebuffer) { + frameCounter++; + if (frameCounter == 5) { + frameCounter = 0; + getAverageFrameColor(); + } + } + } + + @Override + public void onDrawEye(Eye eye) { + eye.applyHeadView(headViewMatrix); + + GLES20.glEnable(GLES20.GL_BLEND); + GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA); + + drawFloor(eye); + drawVideoGlow(eye); + drawVideo(eye); + drawSeekBar(eye); + drawThumbnailSphere(eye, 46); + } + + @Override + public void onFinishFrame(Viewport viewport) {} + + private void drawThumbnail( + int id, boolean active, float thumbnailAngle, float rowAngle, Eye eye) { + if (thumbnailBitmaps[id] == null) { + // Not thumbnail for this id. + return; + } + + int textureId = thumbnailTextureIds[id]; + createGeometry(spriteProgramHolder, thumbnailVertices, textureId); + + // Slow up-down movement animation. + float angleAnimationDiff = (float) Math.sin(animationTime / 4000f + (id * 10f)) * 1.5f; + + setBaseModelMatrix(); + Matrix.rotateM(modelMatrix, 0, thumbnailAngle, 0, 1, 0); + Matrix.rotateM(modelMatrix, 0, rowAngle + angleAnimationDiff, 1, 0, 0); + + float thumbnailDistance = updateAndGetThumbnailDistance(id, active); + Matrix.translateM(modelMatrix, 0, 0, 0, thumbnailDistance); + + // Flip the model for the background so that we can have it upright regardless of the stMatrix + Matrix.rotateM(modelMatrix, 0, 180, 1, 0, 0); + + Matrix.multiplyMM(viewMatrix, 0, eye.getEyeView(), 0, lookAtMatrix, 0); + Matrix.multiplyMM(tmpMatrix, 0, viewMatrix, 0, modelMatrix, 0); + Matrix.multiplyMM(mvpMatrix, 0, eye.getPerspective(Z_NEAR, Z_FAR), 0, tmpMatrix, 0); + + GLES20.glUniformMatrix4fv(spriteProgramHolder.uMvpMatrix, 1, false, mvpMatrix, 0); + GLES20.glUniformMatrix4fv(spriteProgramHolder.uStMatrix, 1, false, spriteStMatrix, 0); + + float alpha = updateAndGetThumbnailAlpha(id); + float brightness = alpha * updateAndGetThumbnailBrightness(id, active); + GLES20.glUniform4f(spriteProgramHolder.uBrightness, brightness, brightness, brightness, 1f); + + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + checkGlError("glDrawArrays"); + + // TODO(kvaalen): merge this now playing gl drawing with the above thumbnail gl drawing. + // They do the same thing, except that the now playing is smaller and lower. + if (id == nowPlayingIndex) { + + createGeometry(spriteProgramHolder, thumbnailVertices, nowPlayingTextureId); + + // Slow up-down movement animation. + angleAnimationDiff = (float) Math.sin(animationTime / 4000f + (id * 10f)) * 1.5f; + + setBaseModelMatrix(); + Matrix.rotateM(modelMatrix, 0, thumbnailAngle, 0, 1, 0); + Matrix.rotateM(modelMatrix, 0, rowAngle + angleAnimationDiff, 1, 0, 0); + + Matrix.translateM(modelMatrix, 0, 0, 0, thumbnailDistance * 0.95f); + + // Flip the model for the background so that we can have it upright regardless of the stMatrix + Matrix.rotateM(modelMatrix, 0, 180, 1, 0, 0); + + Matrix.scaleM(modelMatrix, 0, 0.6f, 0.6f, 0.6f); + + Matrix.multiplyMM(viewMatrix, 0, eye.getEyeView(), 0, lookAtMatrix, 0); + Matrix.multiplyMM(tmpMatrix, 0, viewMatrix, 0, modelMatrix, 0); + Matrix.multiplyMM(mvpMatrix, 0, eye.getPerspective(Z_NEAR, Z_FAR), 0, tmpMatrix, 0); + + GLES20.glUniformMatrix4fv(spriteProgramHolder.uMvpMatrix, 1, false, mvpMatrix, 0); + GLES20.glUniformMatrix4fv(spriteProgramHolder.uStMatrix, 1, false, spriteStMatrix, 0); + GLES20.glUniform4f(spriteProgramHolder.uBrightness, 1.2f, 1.2f, 1.2f, 1.2f); + + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + checkGlError("glDrawArrays"); + } + } + + private float updateAndGetThumbnailAlpha(int id) { + // TODO(kvaalen): Make these animations depend on absolute time, not on the frame rate. + thumbnailAlpha[id] = thumbnailAlpha[id] + ((1f - thumbnailAlpha[id]) / 30f); + return thumbnailAlpha[id]; + } + + private float updateAndGetThumbnailDistance(int id, boolean isActive) { + if (thumbnailDistances[id] == 0f) { + thumbnailDistances[id] = DEFAULT_THUMBNAIL_DISTANCE; + } + + float targetThumbnailDistance = + isActive ? ACTIVE_THUMBNAIL_DISTANCE : DEFAULT_THUMBNAIL_DISTANCE; + + thumbnailDistances[id] = + thumbnailDistances[id] + ((targetThumbnailDistance - thumbnailDistances[id]) / 10f); + + return thumbnailDistances[id]; + } + + private float updateAndGetThumbnailBrightness(int id, boolean isActive) { + if (thumbnailBrightness[id] == 0f) { + thumbnailBrightness[id] = DEFAULT_THUMBNAIL_BRIGHTNESS; + } + + float targetThumbnailBrightness = + isActive ? ACTIVE_THUMBNAIL_BRIGHTNESS : DEFAULT_THUMBNAIL_BRIGHTNESS; + + targetThumbnailBrightness = + (nowPlayingIndex == id) ? NOW_PLAYING_THUMBNAIL_BRIGHTNESS : targetThumbnailBrightness; + + thumbnailBrightness[id] = + thumbnailBrightness[id] + ((targetThumbnailBrightness - thumbnailBrightness[id]) / 10); + + return thumbnailBrightness[id]; + } + + private void drawThumbnailSphere(Eye eye, int thumbnails) { + GLES20.glUseProgram(spriteProgramHolder.program); + checkGlError("glUseProgram"); + + int decreasePerRow = 2; + int unusedOnAllRows = 0; + for (int row = 0; row < ROWS_COUNT; row++) { + unusedOnAllRows += decreasePerRow * (row + 1); + } + + int thumbnailPerRow = (thumbnails + unusedOnAllRows) / ((ROWS_COUNT - 1) * 2 + 1); + + // Number of thumbnail on this row would be decreased by this value. + int unusedOnCurrentRow = 0; + int thumbnailId = 0; + float rowAngle = 0; + int newActiveThumbnail = -1; + for (int row = 0; row < ROWS_COUNT; row++) { + int tiles = thumbnailPerRow - unusedOnCurrentRow; + + for (int x = 0; x < tiles; x++) { + float angleRegion = 360f - SCREEN_ANGLE; + + float anglesPerTile = angleRegion / (tiles - 1); + float thumbnailAngle = (anglesPerTile * x) + (SCREEN_ANGLE / 2); + if (thumbnailAngle > 360f) { + thumbnailAngle -= 360f; + } + + float minRowAngle = -rowAngle - (ROW_ANGLE / 2f); + float maxRowAngle = -rowAngle + (ROW_ANGLE / 2f); + boolean rowActive = (lastPitch >= minRowAngle) && (lastPitch < maxRowAngle); + + float minAngle = thumbnailAngle - (anglesPerTile / 2f) - 180f; + float maxAngle = thumbnailAngle + (anglesPerTile / 2f) - 180f; + + float adjustedMinAngle = minAngle < 0 ? minAngle + 360 : minAngle; + float adjustedMaxAngle = minAngle < 0 ? maxAngle + 360 : maxAngle; + boolean columnActive = (lastYaw >= adjustedMinAngle) && (lastYaw < adjustedMaxAngle); + + drawThumbnail(thumbnailId, columnActive && rowActive, thumbnailAngle, rowAngle, eye); + if (columnActive && rowActive) { + newActiveThumbnail = thumbnailId; + } + thumbnailId += 1; + + // Draw bottom of the sphere, but because view is upsidedown - this is actually top half. + if (row != 0) { + rowActive = false; + minRowAngle = rowAngle - (ROW_ANGLE / 2f); + maxRowAngle = rowAngle + (ROW_ANGLE / 2f); + rowActive = (lastPitch >= minRowAngle) && (lastPitch < maxRowAngle); + drawThumbnail(thumbnailId, columnActive && rowActive, thumbnailAngle, -rowAngle, eye); + if (columnActive && rowActive) { + newActiveThumbnail = thumbnailId; + } + thumbnailId += 1; + } + } + rowAngle += ROW_ANGLE; + unusedOnCurrentRow += 2; + } + + synchronized (this) { + activeThumbnail = newActiveThumbnail; + } + } + + /** + * Create the base model matrix and set it in MMAtrix. The base model matrix is the rotation + * matrix to place the screen in the location where it's thumbnail was. + */ + private void setBaseModelMatrix() { + screenYaw = screenYaw + ((targetScreenYaw - screenYaw) / 800); + Matrix.setRotateM(modelMatrix, 0, screenYaw, 0, 1, 0); + } + + public void setListenStatusActive(boolean active) { + isListening = active; + } + + public boolean isListening() { + return isListening; + } + + private void drawVideo(Eye eye) { + float[] stMatrix; + ProgramHolder programHolder; + int textureId; + + // If the video hasn't yet started. + if (isPlaybackStarting) { + stMatrix = spriteStMatrix; + programHolder = spriteProgramHolder; + + if (hasNoVideos()) { + textureId = noVideoBackgroundTextureId; + } else { + // There's at least one video, so if we're here it means we're starting playback of + // another video, so show a transition texture. + textureId = transitionBackgroundTextureId; + } + } else { + stMatrix = videoStMatrix; + programHolder = videoProgramHolder; + textureId = videoTextureId; + } + + GLES20.glUseProgram(programHolder.program); + checkGlError("glUseProgram"); + createGeometry(programHolder, videoVertices, textureId); + + setBaseModelMatrix(); + + // Scale the video texture according to its aspect ratio. + float horizontalScaling = getVideoAspectRatio() / DEFAULT_ASPECT_RATIO; + Matrix.scaleM(modelMatrix, 0, horizontalScaling, 1.0f, 1.0f); + + Matrix.translateM(modelMatrix, 0, 0, 0, CAMERA_DISTANCE); + + if (isPlaybackStarting) { + // Flip the model for the background so that we can have it upright regardless of the stMatrix + Matrix.rotateM(modelMatrix, 0, 180, 1, 0, 0); + GLES20.glUniform4f(spriteProgramHolder.uBrightness, 1, 1, 1, 1); + } + + Matrix.multiplyMM(viewMatrix, 0, eye.getEyeView(), 0, lookAtMatrix, 0); + Matrix.multiplyMM(tmpMatrix, 0, viewMatrix, 0, modelMatrix, 0); + Matrix.multiplyMM(mvpMatrix, 0, eye.getPerspective(Z_NEAR, Z_FAR), 0, tmpMatrix, 0); + + GLES20.glUniformMatrix4fv(programHolder.uMvpMatrix, 1, false, mvpMatrix, 0); + GLES20.glUniformMatrix4fv(programHolder.uStMatrix, 1, false, stMatrix, 0); + + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + checkGlError("glDrawArrays"); + } + + private void drawFloor(Eye eye) { + GLES20.glUseProgram(spriteProgramHolder.program); + checkGlError("glUseProgram"); + createGeometry(spriteProgramHolder, floorVertices, floorTextureId); + + setBaseModelMatrix(); + + Matrix.rotateM(modelMatrix, 0, 90, 1, 0, 0); + Matrix.translateM(modelMatrix, 0, 0, 0, CAMERA_DISTANCE + 1.0f); + + GLES20.glUniform4f(spriteProgramHolder.uBrightness, 1, 1, 1, 1); + + Matrix.multiplyMM(viewMatrix, 0, eye.getEyeView(), 0, lookAtMatrix, 0); + Matrix.multiplyMM(tmpMatrix, 0, viewMatrix, 0, modelMatrix, 0); + Matrix.multiplyMM(mvpMatrix, 0, eye.getPerspective(Z_NEAR, Z_FAR), 0, tmpMatrix, 0); + + GLES20.glUniformMatrix4fv(spriteProgramHolder.uMvpMatrix, 1, false, mvpMatrix, 0); + GLES20.glUniformMatrix4fv(spriteProgramHolder.uStMatrix, 1, false, spriteStMatrix, 0); + + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + checkGlError("glDrawArrays"); + } + + private void drawSeekBar(Eye eye) { + GLES20.glUseProgram(spriteProgramHolder.program); + checkGlError("glUseProgram"); + createGeometry(spriteProgramHolder, seekBarVertices, seekBarTextureId); + + setBaseModelMatrix(); + Matrix.translateM(modelMatrix, 0, 0, 0, CAMERA_DISTANCE); + Matrix.translateM(modelMatrix, 0, HALF_VIDEO_WIDTH, 0, 0); + Matrix.scaleM(modelMatrix, 0, seekBarRatio, 1f, 1f); + + Matrix.multiplyMM(viewMatrix, 0, eye.getEyeView(), 0, lookAtMatrix, 0); + Matrix.multiplyMM(mvpMatrix, 0, viewMatrix, 0, modelMatrix, 0); + Matrix.multiplyMM(mvpMatrix, 0, eye.getPerspective(Z_NEAR, Z_FAR), 0, mvpMatrix, 0); + + GLES20.glUniformMatrix4fv(spriteProgramHolder.uMvpMatrix, 1, false, mvpMatrix, 0); + GLES20.glUniformMatrix4fv(spriteProgramHolder.uStMatrix, 1, false, spriteStMatrix, 0); + GLES20.glUniform4f(spriteProgramHolder.uBrightness, 1, 1, 1, 1); + + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + checkGlError("glDrawArrays"); + } + + /** Draws the glowing border around the video. */ + private void drawVideoGlow(Eye eye) { + GLES20.glUseProgram(spriteProgramHolder.program); + checkGlError("glUseProgram"); + createGeometry(spriteProgramHolder, videoVertices, videoGlowTextureId); + + setBaseModelMatrix(); + Matrix.translateM(modelMatrix, 0, 0, 0, CAMERA_DISTANCE); + float glowTextureScale = 0.2f; + Matrix.scaleM(modelMatrix, 0, 1f + glowTextureScale, 1f + glowTextureScale * (16f / 9f), 1f); + Matrix.multiplyMM(viewMatrix, 0, eye.getEyeView(), 0, lookAtMatrix, 0); + Matrix.multiplyMM(tmpMatrix, 0, viewMatrix, 0, modelMatrix, 0); + Matrix.multiplyMM(mvpMatrix, 0, eye.getPerspective(Z_NEAR, Z_FAR), 0, tmpMatrix, 0); + + GLES20.glUniformMatrix4fv(spriteProgramHolder.uMvpMatrix, 1, false, mvpMatrix, 0); + GLES20.glUniformMatrix4fv(spriteProgramHolder.uStMatrix, 1, false, spriteStMatrix, 0); + + GLES20.glUniform4f( + spriteProgramHolder.uBrightness, glowValues[0], glowValues[1], glowValues[2], 1f); + + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + checkGlError("glDrawArrays"); + } + + private void createGeometry(ProgramHolder programHolder, FloatBuffer vertices, int textureId) { + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + int glTextureType = + textureId == videoTextureId ? GL_TEXTURE_EXTERNAL_OES : GLES20.GL_TEXTURE_2D; + GLES20.glBindTexture(glTextureType, textureId); + + vertices.position(TRIANGLE_VERTICES_DATA_POS_OFFSET); + GLES20.glVertexAttribPointer( + programHolder.aPosition, + 3, + GLES20.GL_FLOAT, + false, + TRIANGLE_VERTICES_DATA_STRIDE_BYTES, + vertices); + checkGlError("glVertexAttribPointer aPosition"); + GLES20.glEnableVertexAttribArray(programHolder.aPosition); + checkGlError("glEnableVertexAttribArray aPosition handle"); + + vertices.position(TRIANGLE_VERTICES_DATA_UV_OFFSET); + GLES20.glVertexAttribPointer( + programHolder.aTexture, + 3, + GLES20.GL_FLOAT, + false, + TRIANGLE_VERTICES_DATA_STRIDE_BYTES, + vertices); + checkGlError("glVertexAttribPointer aTexture handle"); + GLES20.glEnableVertexAttribArray(programHolder.aTexture); + checkGlError("glEnableVertexAttribArray aTexture handle"); + } + + private void createFsqGeometry( + FsqProgramHolder programHolder, FloatBuffer vertices, int textureId) { + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + int glTextureType = + textureId == videoTextureId ? GL_TEXTURE_EXTERNAL_OES : GLES20.GL_TEXTURE_2D; + GLES20.glBindTexture(glTextureType, textureId); + + vertices.position(TRIANGLE_VERTICES_DATA_POS_OFFSET); + GLES20.glVertexAttribPointer( + programHolder.aPosition, + 3, + GLES20.GL_FLOAT, + false, + TRIANGLE_VERTICES_DATA_STRIDE_BYTES, + vertices); + checkGlError("glVertexAttribPointer aPosition"); + GLES20.glEnableVertexAttribArray(programHolder.aPosition); + checkGlError("glEnableVertexAttribArray aPosition handle"); + } + + private void getAverageFrameColor() { + if (isPlaybackStarting) { + return; + } + + // Save the current framebuffer so we can restore it at the end of this method + // (necessary for this code to work with distortion correction enabled). + int[] params = new int[1]; + GLES20.glGetIntegerv(GLES20.GL_FRAMEBUFFER_BINDING, params, 0); + int currentFrameBuffer = params[0]; + + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, framebuffer); + GLES20.glViewport(0, 0, FBO_WIDTH, FBO_HEIGHT); + GLES20.glScissor(0, 0, FBO_WIDTH, FBO_HEIGHT); + + checkGlError("glBindFramebuffer"); + GLES20.glClearColor(0f, 0f, 0f, 1f); + GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT); + + GLES20.glUseProgram(fsqProgramHolder.program); + checkGlError("glUseProgram"); + createFsqGeometry(fsqProgramHolder, lightboxVertices, videoTextureId); + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + checkGlError("glDrawArrays"); + + framePixels.clear(); + GLES20.glReadPixels( + 0, 0, FBO_WIDTH, FBO_HEIGHT, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, framePixels); + checkGlError("glReadPixels"); + + setGlowValues(0, 0, 0); + while (framePixels.hasRemaining()) { + glowValues[0] += framePixels.get() & 0xFF; + glowValues[1] += framePixels.get() & 0xFF; + glowValues[2] += framePixels.get() & 0xFF; + framePixels.get(); // Read the alpha to advance the buffer. + } + + for (int i = 0; i < 3; i++) { + // Clamp the value so there is always some glow. + glowValues[i] = + max( + MIN_VIDEO_GLOW_BRIGHTNESS, + glowValues[i] / 255f / (FBO_WIDTH * FBO_HEIGHT * 3.0f) * 2.0f); + } + + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, currentFrameBuffer); + } + + @Override + public void onSurfaceChanged(int width, int height) { + // Nothing to do. Handled by GvrView. + } + + @Override + public void onSurfaceCreated(EGLConfig config) { + initialTime = SystemClock.uptimeMillis(); + videoProgramHolder = createProgramHolder(VIDEO_FRAGMENT_SHADER, false); + spriteProgramHolder = createProgramHolder(FRAGMENT_SHADER, true); + fsqProgramHolder = createFsqProgramHolder(FSQ_VERTEX_SHADER, FSQ_FRAGMENT_SHADER); + + Matrix.setLookAtM( + lookAtMatrix, 0, 0, CAMERA_HEIGHT, 0, 0, CAMERA_HEIGHT, CAMERA_DISTANCE, 0, 1, 0); + Matrix.setIdentityM(videoStMatrix, 0); + Matrix.setIdentityM(spriteStMatrix, 0); + + textureIds = new int[NUM_SPRITE_TEXTURES]; + GLES20.glGenTextures(NUM_SPRITE_TEXTURES, textureIds, 0); + + // Create the texture used for transitions between videos. + transitionBackgroundTextureId = textureIds[0]; + createResourceTexture(transitionBackgroundTextureId, R.raw.transition_bg); + + // Create the texture used when there is no video. + noVideoBackgroundTextureId = textureIds[1]; + createResourceTexture(noVideoBackgroundTextureId, R.raw.novideos_bg); + + // Create the texture used by the video background logo. + videoGlowTextureId = textureIds[2]; + createResourceTexture(videoGlowTextureId, R.raw.glow_bg); + + // Create the texture used by the video background logo. + nowPlayingTextureId = textureIds[3]; + createResourceTexture(nowPlayingTextureId, R.raw.playing); + + // Create the texture used by the seekbar. + seekBarTextureId = textureIds[4]; + createResourceTexture(seekBarTextureId, R.raw.seekbar); + + // Create the texture used for video playback. + videoTextureId = textureIds[5]; + GLES20.glBindTexture(GL_TEXTURE_EXTERNAL_OES, videoTextureId); + + // Create the texture used for floor. + floorTextureId = textureIds[6]; + createResourceTexture(floorTextureId, R.raw.floor); + + checkGlError("glBindTexture mTextureID"); + + GLES20.glTexParameterf( + GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST); + GLES20.glTexParameterf(GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + + surfaceTexture = new SurfaceTexture(videoTextureId); + surfaceTexture.setOnFrameAvailableListener(this); + + surfaceTextureListener.onSurfaceTextureCreated(surfaceTexture); + + synchronized (this) { + surfaceNeedsUpdate = false; + } + + fboTargetTextureId = createFboTargetTexture(FBO_WIDTH, FBO_HEIGHT); + framebuffer = createFrameBuffer(fboTargetTextureId); + if (framebuffer != -1) { + framePixels = ByteBuffer.allocateDirect(FBO_WIDTH * FBO_HEIGHT * 4); + framePixels.order(ByteOrder.nativeOrder()); + enableFramebuffer = true; + } + + // Update the thumbnails that have been loaded so far. + thumbnailTextureIds = new int[NUM_THUMBNAIL_TEXTURES]; + GLES20.glGenTextures(NUM_THUMBNAIL_TEXTURES, thumbnailTextureIds, 0); + for (int i = 0; i < NUM_THUMBNAIL_TEXTURES; i++) { + if (thumbnailBitmaps[i] != null) { + loadThumbnailTexture(i); + } + } + } + + private int createFboTargetTexture(int width, int height) { + fboTargetTextureIds = new int[1]; + GLES20.glGenTextures(1, fboTargetTextureIds, 0); + int texture = fboTargetTextureIds[0]; + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture); + GLES20.glTexImage2D( + GLES20.GL_TEXTURE_2D, + 0, + GLES20.GL_RGBA, + width, + height, + 0, + GLES20.GL_RGBA, + GLES20.GL_UNSIGNED_BYTE, + null); + + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + return texture; + } + + private int createFrameBuffer(int fboTargetTextureId) { + // Save the current framebuffer so we can restore it at the end of this method + // (necessary for this code to work with distortion correction enabled). + int[] params = new int[1]; + GLES20.glGetIntegerv(GLES20.GL_FRAMEBUFFER_BINDING, params, 0); + int currentFrameBuffer = params[0]; + + int[] framebuffers = new int[1]; + GLES20.glGenFramebuffers(1, framebuffers, 0); + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, framebuffers[0]); + + int[] renderbuffers = new int[1]; + GLES20.glGenRenderbuffers(1, renderbuffers, 0); + + GLES20.glFramebufferTexture2D( + GLES20.GL_FRAMEBUFFER, + GLES20.GL_COLOR_ATTACHMENT0, + GLES20.GL_TEXTURE_2D, + fboTargetTextureId, + 0); + int status = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER); + + if (status != GLES20.GL_FRAMEBUFFER_COMPLETE) { + logger.atSevere().log("Framebuffer is not complete: %x", status); + return -1; + } + + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, currentFrameBuffer); + return framebuffers[0]; + } + + private void loadThumbnailTexture(int index) { + if (thumbnailTextureIds == null) { + // The thumbnail textures will get loaded later once the thumbnailTextureIds are generated. + return; + } + + int textureId = thumbnailTextureIds[index]; + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId); + + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT); + + GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, thumbnailBitmaps[index], 0); + } + + private void createResourceTexture(int textureId, int resourceId) { + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId); + + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT); + + InputStream is = context.getResources().openRawResource(resourceId); + Bitmap bitmap; + try { + bitmap = BitmapFactory.decodeStream(is); + } finally { + try { + is.close(); + } catch (IOException e) { + // Ignore. + } + } + + GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0); + bitmap.recycle(); + } + + @Override + public synchronized void onFrameAvailable(SurfaceTexture surface) { + // We received a frame, so it means we have at least one valid video. + setNoVideos(false); + surfaceNeedsUpdate = true; + } + + /** Creates a GL program and the associated handlers from a given fragment shader. */ + private ProgramHolder createProgramHolder(String fragmentShader, boolean supportsBrightness) { + ProgramHolder holder = new ProgramHolder(); + holder.program = createProgram(VERTEX_SHADER, fragmentShader); + if (holder.program == 0) { + throw new VerifyException("Could not create program"); + } + + holder.aPosition = GLES20.glGetAttribLocation(holder.program, "aPosition"); + checkGlError("glGetAttribLocation aPosition"); + if (holder.aPosition == -1) { + throw new VerifyException("Could not get attrib location for aPosition"); + } + holder.aTexture = GLES20.glGetAttribLocation(holder.program, "aTextureCoord"); + checkGlError("glGetAttribLocation aTextureCoord"); + if (holder.aTexture == -1) { + throw new VerifyException("Could not get attrib location for aTextureCoord"); + } + + holder.uMvpMatrix = GLES20.glGetUniformLocation(holder.program, "uMvpMatrix"); + checkGlError("glGetUniformLocation uMvpMatrix"); + if (holder.uMvpMatrix == -1) { + throw new VerifyException("Could not get attrib location for uMvpMatrix"); + } + + holder.uStMatrix = GLES20.glGetUniformLocation(holder.program, "uStMatrix"); + checkGlError("glGetUniformLocation uStMatrix"); + if (holder.uStMatrix == -1) { + throw new VerifyException("Could not get attrib location for uStMatrix"); + } + + if (supportsBrightness) { + holder.uBrightness = GLES20.glGetUniformLocation(holder.program, "uBrightness"); + checkGlError("glGetUniformLocation uBrightness"); + if (holder.uBrightness == -1) { + throw new VerifyException("Could not get attrib location for uBrightness"); + } + } + + return holder; + } + + private FsqProgramHolder createFsqProgramHolder(String vertexShader, String fragmentShader) { + FsqProgramHolder holder = new FsqProgramHolder(); + holder.program = createProgram(vertexShader, fragmentShader); + if (holder.program == 0) { + throw new VerifyException("Could not create program"); + } + + holder.aPosition = GLES20.glGetAttribLocation(holder.program, "aPosition"); + checkGlError("glGetAttribLocation aPosition"); + if (holder.aPosition == -1) { + throw new VerifyException("Could not get attrib location for aPosition"); + } + return holder; + } + + private int loadShader(int shaderType, String source) { + int shader = GLES20.glCreateShader(shaderType); + if (shader != 0) { + GLES20.glShaderSource(shader, source); + GLES20.glCompileShader(shader); + int[] compiled = new int[1]; + GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0); + if (compiled[0] == 0) { + logger.atSevere().log( + "Could not compile shader %s %s", shaderType, GLES20.glGetShaderInfoLog(shader)); + GLES20.glDeleteShader(shader); + shader = 0; + } + } + return shader; + } + + private int createProgram(String vertexSource, String fragmentSource) { + int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource); + if (vertexShader == 0) { + return 0; + } + int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource); + if (pixelShader == 0) { + return 0; + } + + int program = GLES20.glCreateProgram(); + if (program != 0) { + GLES20.glAttachShader(program, vertexShader); + checkGlError("glAttachShader"); + GLES20.glAttachShader(program, pixelShader); + checkGlError("glAttachShader"); + GLES20.glLinkProgram(program); + int[] linkStatus = new int[1]; + GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0); + if (linkStatus[0] != GLES20.GL_TRUE) { + logger.atSevere().log("Could not link program: %s", GLES20.glGetProgramInfoLog(program)); + GLES20.glDeleteProgram(program); + program = 0; + } + } + return program; + } + + private void checkGlError(String op) { + int error; + while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) { + logger.atSevere().log("%s : glError: %s", op, error); + throw new VerifyException(op + ": glError " + error); + } + } + + @Override + public void onRendererShutdown() { + GLES20.glDeleteTextures(NUM_SPRITE_TEXTURES, textureIds, 0); + GLES20.glDeleteTextures(NUM_THUMBNAIL_TEXTURES, thumbnailTextureIds, 0); + GLES20.glDeleteTextures(1, fboTargetTextureIds, 0); + + GLES20.glDeleteProgram(videoProgramHolder.program); + GLES20.glDeleteProgram(spriteProgramHolder.program); + GLES20.glDeleteProgram(fsqProgramHolder.program); + } +} diff --git a/paperscope/myvideos/VideoFetcher.java b/paperscope/myvideos/VideoFetcher.java new file mode 100644 index 00000000..823195f9 --- /dev/null +++ b/paperscope/myvideos/VideoFetcher.java @@ -0,0 +1,106 @@ +package com.google.vr.cardboard.paperscope.demo.myvideos; + +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.MediaStore; +import com.google.common.flogger.FluentLogger; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * A basic video fetcher for videos stored on the phone. + * TODO(dcoz): test this class. + */ +public class VideoFetcher { + + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + /** Simple structure with information about a video stored on the phone file system. */ + public static class VideoInfo { + + /** Stream for the video. */ + private Uri videoStreamUri; + + /** The absolute file path to the video file. */ + private String videoAbsolutePath; + + public VideoInfo(Uri videoStreamUri, String videoAbsolutePath) { + this.videoStreamUri = videoStreamUri; + this.videoAbsolutePath = videoAbsolutePath; + } + + public Uri getVideoStreamUri() { + return videoStreamUri; + } + + public String getVideoAbsolutePath() { + return videoAbsolutePath; + } + } + + /** + * Interface used to communicate video results back to requesters. + */ + public interface Callback { + /** + * Called when a video list response was obtained successfully. + */ + void onResponse(List response); + + /** + * Called when an exception is caught while getting the response. + */ + void onError(Exception exception); + } + + private final Callback videoListCallback; + private Cursor videoCursor; + private Context context; + + public VideoFetcher(Context context, Callback videoListCallback) { + this.context = context; + this.videoListCallback = videoListCallback; + } + + /** + * Fetches the local videos stored on the phone. On success, the list of videos are sent to + * the video callback specified in the constructor. + */ + public void getLocalVideos() { + // Prepare a query for local videos, sorted by date. + // TODO(dcoz): figure out what we should do for videos that don't have the expected 'landscape' + // aspect ratio. + + String[] projection = { + MediaStore.Video.Media._ID, MediaStore.Video.Media.DATA, + }; + videoCursor = context.getContentResolver().query( + MediaStore.Video.Media.EXTERNAL_CONTENT_URI, + projection, + null, + null, + MediaStore.MediaColumns.DATE_ADDED + " DESC"); + + if (videoCursor == null) { + logger.atWarning().log("Error querying local videos."); + videoListCallback.onError(new IOException("Error querying local videos.")); + } + + int idColumn = videoCursor.getColumnIndex(MediaStore.Video.Media._ID); + int dataColumn = videoCursor.getColumnIndex(MediaStore.Video.Media.DATA); + + List videoInfoList = new ArrayList(); + while (videoCursor.moveToNext()) { + Uri contentUri = Uri.parse( + android.provider.MediaStore.Video.Media.EXTERNAL_CONTENT_URI + + "/" + videoCursor.getString(idColumn)); + + videoInfoList.add(new VideoInfo(contentUri, videoCursor.getString(dataColumn))); + } + + videoListCallback.onResponse(videoInfoList); + } + +} diff --git a/paperscope/myvideos/VideoSequencer.java b/paperscope/myvideos/VideoSequencer.java new file mode 100644 index 00000000..3cb38e3e --- /dev/null +++ b/paperscope/myvideos/VideoSequencer.java @@ -0,0 +1,204 @@ +package com.google.vr.cardboard.paperscope.demo.myvideos; + +import android.content.Context; +import android.util.Pair; +import com.google.cardboard.sdk.CardboardView; +import com.google.common.flogger.FluentLogger; +import com.google.vr.cardboard.paperscope.demo.myvideos.VideoFetcher.VideoInfo; +import java.util.ArrayList; +import java.util.List; + +/** + * Sequencer for fetching and playing videos. + */ +public class VideoSequencer implements MyVideosPlayer.Listener { + + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + /** + * When an error happens, we skip to the next video. To ensure we don't hit the server too much + * if something goes wrong, we limit the number of skips. + */ + private static final int MAX_CONSECUTIVE_ERRORS_ALLOWED = 20; + + private final CardboardView cardboardView; + private final MyVideosRenderer renderer; + private final MyVideosPlayer player; + private final VideoFetcher videoFetcher; + private final ImageFetcher imageFetcher; + + private List videoList; + private int currentVideoIndex; + private int numThumbnailsFetched; + private int consecutiveErrorCount; + + public VideoSequencer(Context context, CardboardView cardboardView, MyVideosRenderer renderer) { + this.cardboardView = cardboardView; + this.renderer = renderer; + + player = new MyVideosPlayer(context, this /* Listener */, renderer); + videoFetcher = new VideoFetcher(context, new VideoListCallback()); + imageFetcher = new ImageFetcher(context, cardboardView, renderer); + + renderer.setSurfaceTextureListener(player); + } + + public void startPlayback() { + if (videoList != null && !videoList.isEmpty()) { + currentVideoIndex = currentVideoIndex < 0 ? 0 : currentVideoIndex; + player.play(videoList.get(currentVideoIndex)); + } else { + imageFetcher.reset(); + currentVideoIndex = -1; + maybeLoadMoreVideos(); + } + } + + public void stop() { + if (player != null) { + player.stop(); + } + } + + public void togglePause() { + if (player != null) { + player.togglePause(); + } + } + + public void play() { + if (player != null) { + player.play(); + } + } + + public void pause() { + if (player != null) { + player.pause(); + } + } + + public void release() { + player.release(); + } + + public void playTargetedVideo() { + // Might be null if the trigger is pulled before any video is loaded. + if (videoList == null) { + return; + } + + final int videoIndex = renderer.getActiveThumbnailId(); + if (videoIndex < 0 || videoIndex >= videoList.size()) { + logger.atSevere().log("Video index out of bounds"); + return; + } + + cardboardView.queueEvent(new Runnable() { + @Override + public void run() { + renderer.onThumbnailSelected(videoIndex); + } + }); + playVideoByIndex(videoIndex); + } + + /** + * Load local videos. + */ + private void maybeLoadMoreVideos() { + if (videoList == null) { + videoFetcher.getLocalVideos(); + } + } + + private void playNext() { + if (videoList != null) { + playVideoByIndex((currentVideoIndex + 1) % videoList.size()); + } + } + + private void playVideoByIndex(int index) { + if (videoList == null) { + return; + } + + if (index < 0 || index >= videoList.size()) { + logger.atSevere().log( + "Tried to play video at index: %d , but video list size is %d", index, videoList.size()); + return; + } + + currentVideoIndex = index; + player.play(videoList.get(index)); + renderer.setCurrentVideoIndex(index); + } + + private void fetchThumbnails(List videoList, int startIndex) { + int index = startIndex; + List> thumbnailUrls = new ArrayList<>(videoList.size()); + for (VideoInfo videoInfo : videoList) { + if (numThumbnailsFetched++ >= renderer.getMaxThumbnailCount()) { + break; + } + thumbnailUrls.add(Pair.create(index++, videoInfo)); + } + imageFetcher.downloadImages(thumbnailUrls); + } + + // MyVideosPlayer.Listener + + @Override + public void onVideoEnded() { + playNext(); + } + + @Override + public void onPlaybackError(int what, int extra) { + if (consecutiveErrorCount++ < MAX_CONSECUTIVE_ERRORS_ALLOWED) { + playNext(); + } else { + // TODO(kvaalen): Show a visual error message. + player.stop(); + } + } + + @Override + public void onPlaybackSucceeded() { + consecutiveErrorCount = 0; + } + + /** + * Callback listening to streams from a GData request and plays the appropriate one. + */ + private class VideoListCallback implements VideoFetcher.Callback { + + @Override + public void onResponse(List videoList) { + if (!videoList.isEmpty()) { + int thumbnailStartIndex; + if (VideoSequencer.this.videoList == null) { + thumbnailStartIndex = 0; + VideoSequencer.this.videoList = videoList; + playVideoByIndex(0); + } else { + thumbnailStartIndex = VideoSequencer.this.videoList.size(); + // Append the results so that we can request several categories. + VideoSequencer.this.videoList.addAll(videoList); + } + fetchThumbnails(videoList, thumbnailStartIndex); + maybeLoadMoreVideos(); + } else { + onError(new Exception("Video list is empty")); + } + } + + @Override + public void onError(Exception exception) { + // TODO(kvaalen): Show some type of visual error. + logger.atSevere().withCause(exception).log("Error getting videos"); + } + + } + +} diff --git a/paperscope/myvideos/res/raw/floor.png b/paperscope/myvideos/res/raw/floor.png new file mode 100644 index 00000000..8bc0d1fa Binary files /dev/null and b/paperscope/myvideos/res/raw/floor.png differ diff --git a/paperscope/myvideos/res/raw/glow_bg.png b/paperscope/myvideos/res/raw/glow_bg.png new file mode 100644 index 00000000..f67d4199 Binary files /dev/null and b/paperscope/myvideos/res/raw/glow_bg.png differ diff --git a/paperscope/myvideos/res/raw/novideos_bg.png b/paperscope/myvideos/res/raw/novideos_bg.png new file mode 100644 index 00000000..972f0fcd Binary files /dev/null and b/paperscope/myvideos/res/raw/novideos_bg.png differ diff --git a/paperscope/myvideos/res/raw/playing.png b/paperscope/myvideos/res/raw/playing.png new file mode 100644 index 00000000..a51d57e8 Binary files /dev/null and b/paperscope/myvideos/res/raw/playing.png differ diff --git a/paperscope/myvideos/res/raw/seekbar.png b/paperscope/myvideos/res/raw/seekbar.png new file mode 100644 index 00000000..19ba9d9a Binary files /dev/null and b/paperscope/myvideos/res/raw/seekbar.png differ diff --git a/paperscope/myvideos/res/raw/transition_bg.png b/paperscope/myvideos/res/raw/transition_bg.png new file mode 100644 index 00000000..99b603bc Binary files /dev/null and b/paperscope/myvideos/res/raw/transition_bg.png differ diff --git a/paperscope/paperscope_launcher/common/strings/localized_strings.cc b/paperscope/paperscope_launcher/common/strings/localized_strings.cc new file mode 100644 index 00000000..e3b5e2e5 --- /dev/null +++ b/paperscope/paperscope_launcher/common/strings/localized_strings.cc @@ -0,0 +1,46 @@ +#include "../common/strings/localized_strings.h" + +#include +#include +#include +#include + +namespace cardboard_demo { + +std::vector GetProbableLocalesInOrder(std::string locale) { + std::vector out; + // Convert BCP 47 codes (dash separators) to ISO 15897 codes (underscores), + // to match the canonical format used by Translation Console. + std::replace(locale.begin(), locale.end(), '-', '_'); + + // First, try using the ISO 15897 locale (e.g. "pt_BR"). + out.push_back(locale); + +#if defined(ION_PLATFORM_IOS) || defined(ION_PLATFORM_ANDROID) + // Replace common iOS script IDs with canonical ISO 15897 codes. + if (locale == "zh_Hans") { + locale = "zh_CN"; + } else if (locale == "zh_Hant") { + locale = "zh_TW"; + } + out.push_back(locale); +#endif // defined(ION_PLATFORM_IOS) || defined(ION_PLATFORM_ANDROID) + + // Then look up the locale's ISO 639 alpha-2 code (2 letter code, e.g. "pt"). + if (locale.size() > 2) { + locale.resize(2); + out.push_back(locale); + } + + // Otherwise, fallback to ISO 639-1 English ("en") which must be present in + // any vr::LocaleMap. + out.push_back("en"); + return out; +} + +const char* GetLocalizedStringFromMap(std::string_view key, + const LocaleEntry& entry) { + auto iter = entry.find(key.data()); + return (iter == entry.end()) ? nullptr : iter->second.c_str(); +} +} // namespace cardboard_demo diff --git a/paperscope/paperscope_launcher/common/strings/localized_strings.h b/paperscope/paperscope_launcher/common/strings/localized_strings.h new file mode 100644 index 00000000..6de60d13 --- /dev/null +++ b/paperscope/paperscope_launcher/common/strings/localized_strings.h @@ -0,0 +1,30 @@ +// Used by the code generated by vr/apps/common/tools/genstrings.py. + +#ifndef PAPERSCOPE_LAUNCHER_COMMON_STRINGS_LOCALIZED_STRINGS_H_ +#define PAPERSCOPE_LAUNCHER_COMMON_STRINGS_LOCALIZED_STRINGS_H_ + +#include +#include +#include +#include + +namespace cardboard_demo { +typedef std::unordered_map LocaleEntry; +typedef std::unordered_map LocaleMap; + +// Returns the locale strings to try to try for a given locale in order of best +// choice to worst. +// This should be used for retrieving LocaleEntrys from a LocaleMap which is +// autogenerated by //vr/apps/common/tools/localization/genstrings.py. +// "en" is required in any LocaleMap and will thus always be included at the +// end of the vector as a last resort. +std::vector GetProbableLocalesInOrder(std::string locale); + +// Returns localized string for |key| in the |entry| if it exists, otherwise +// returns nullptr. +const char* GetLocalizedStringFromMap(std::string_view key, + const LocaleEntry& entry); +} // namespace cardboard_demo + + +#endif // PAPERSCOPE_LAUNCHER_COMMON_STRINGS_LOCALIZED_STRINGS_H_ diff --git a/paperscope/paperscope_launcher/common/strings/localized_strings_test.cc b/paperscope/paperscope_launcher/common/strings/localized_strings_test.cc new file mode 100644 index 00000000..35cb35f9 --- /dev/null +++ b/paperscope/paperscope_launcher/common/strings/localized_strings_test.cc @@ -0,0 +1,110 @@ +#include "vr/apps/cardboard/demo/common/strings/localized_strings.h" + +#include +#include + +#include "testing/base/public/gunit.h" + +using cardboard_demo::GetLocalizedStringFromMap; +using cardboard_demo::GetProbableLocalesInOrder; +using cardboard_demo::LocaleMap; + +namespace { +const cardboard_demo::LocaleEntry& GetLocalizedStringsMap( + const std::string& target_locale, + cardboard_demo::LocaleMap& locale_map) { + const std::vector locales = + GetProbableLocalesInOrder(target_locale); + for (auto& locale : locales) { + if (locale_map.find(locale) != locale_map.end()) { + return locale_map.at(locale); + } + } + // Fallback to last locale option. + return locale_map.at(locales[locales.size() - 1]); +} +} // namespace + +TEST(LocalizedStrings, GetProbableLocalesInOrder) { + LocaleMap locale_map = { + { + "en", + { + {"1", "one"}, + {"2", "two"}, + }, + }, + { + "fr", + { + {"1", "une"}, + {"2", "deux"}, + }, + }, + }; + std::vector locales = GetProbableLocalesInOrder("fr"); + // Expecting ["fr", "en"]. + EXPECT_EQ(locales.size(), 2); + EXPECT_EQ("fr", locales[0]); + EXPECT_EQ("en", locales[1]); + locales = GetProbableLocalesInOrder("en"); + // Expecting "en" to be first (may be 2 "en" instances). + EXPECT_GE(locales.size(), 1); + EXPECT_EQ("en", locales[0]); +} + +TEST(LocalizedStrings, GetLocalizedStringFromMap) { + LocaleMap locale_map = { + { + "en", + { + {"1", "one"}, + {"2", "two"}, + }, + }, + { + "fr", + { + {"1", "une"}, + {"2", "deux"}, + }, + }, + }; + LocaleMap en_map = { + { + "en", + { + {"3", "three"}, + {"4", "four"}, + }, + }, + }; + auto locale = GetLocalizedStringsMap("en", locale_map); + EXPECT_EQ("one", locale["1"]); + EXPECT_EQ("two", locale["2"]); + EXPECT_EQ(locale.end(), locale.find("3")); + EXPECT_STREQ("one", GetLocalizedStringFromMap("1", locale)); + EXPECT_STREQ("two", GetLocalizedStringFromMap("2", locale)); + EXPECT_EQ(nullptr, GetLocalizedStringFromMap("3", locale)); + + locale = GetLocalizedStringsMap("en", en_map); + EXPECT_EQ("three", locale["3"]); + EXPECT_EQ("four", locale["4"]); + EXPECT_EQ(locale.end(), locale.find("1")); + + locale = GetLocalizedStringsMap("fr", locale_map); + EXPECT_EQ("une", locale["1"]); + EXPECT_EQ("deux", locale["2"]); + EXPECT_EQ(locale.end(), locale.find("3")); + + locale = GetLocalizedStringsMap("fr_ext", locale_map); + EXPECT_EQ("une", locale["1"]); + EXPECT_EQ("deux", locale["2"]); + EXPECT_EQ(locale.end(), locale.find("3")); + + // Test that we fall back to English. + locale = GetLocalizedStringsMap("sp", locale_map); + EXPECT_EQ("one", locale["1"]); + EXPECT_EQ("two", locale["2"]); + EXPECT_EQ(locale.end(), locale.find("3")); +} diff --git a/paperscope/paperscope_launcher/common/tools/genstrings.py b/paperscope/paperscope_launcher/common/tools/genstrings.py new file mode 100755 index 00000000..6e102f49 --- /dev/null +++ b/paperscope/paperscope_launcher/common/tools/genstrings.py @@ -0,0 +1,487 @@ +#!/usr/bin/python +"""A script which can be used to generate code to easily load strings. + +Run this script inside a custom build step with: + genstrings.py + --resource_file=resources/strings/en.lproj/MyStrings.strings + --namespace=mystrings + --out_dir=resources/strings/ +TODO(bwuest): Add unit test. b/24000998 +""" + +import optparse +import os +import re +import sys +from typing import Dict, List + + +class Error(Exception): + """The base call of all exceptions in this module.""" + pass + + +class StringsParser(object): + """A class which can be used to parse xcode .strings files.""" + + def __init__(self, resource_file: str, namespace: str, out_dir: str, + check_substitution_tokens: bool, language: str) -> None: + """Initializes parser with the resource file to use. + + Args: + resource_file: The name of the Localizeable.strings file. + namespace: The namespace used for the generated classes. + out_dir: The directory where the generated code should go. + check_substitution_tokens: Whether or not to validate that the + substitution tokens are valid. + language: Target language for code generation. + """ + self._resource_file_name = resource_file + self._namespace = namespace + self._out_dir = out_dir + self._parsed_strings = {} + self._check_substitution_tokens = check_substitution_tokens + self._language = language + + def Run(self): + """Parses the resource file, and generates the output files.""" + self._ParseFile() + if self._language == 'cc': + self._GenerateCc() + elif self._language == 'java': + self._GenerateJava() + else: + raise Error('Invalid language specified') + + def _GenerateEnumConstant(self, string_key): + """Generates a string constant from a string key. + + Args: + string_key: The key for the string. This needs to be converted into + a camelcase string, prefixed by kStr. + + Returns: + The enum constant for the given string key. + """ + return 'kStr' + ''.join([part[0].upper() + part[1:] + for part in string_key.split('_')]) + + def _GenerateCc(self) -> None: + """Generates the contents of the C++ output files, from the parsed input.""" + + locale_map = self._GetFullLocaleMap() + + locale_map_initialization_functions = [] + locale_map_initialization = [] + # Inserting locales one at a time to the map, to avoid having more than one + # on the stack at a given time, which would violate stack frame size checks. + for locale_id, string_map in locale_map.items(): + string_map = [ + ' this_language_map["%s"] = "%s";' % i + for i in string_map.items() + ] + + locale_map_initialization_functions.append("""\ + void Add%(locale_id)sToMap(cardboard_demo::LocaleMap& locale_map) { + auto& this_language_map = locale_map["%(locale_id)s"]; +%(string_map)s + } + """ % dict(locale_id=locale_id, string_map='\n'.join(string_map))) + + locale_map_initialization.append("""\ + if (locale == "%(locale_id)s") { + Add%(locale_id)sToMap(locale_map); + }""" % dict(locale_id=locale_id)) + + if not os.path.exists(self._out_dir): + os.makedirs(self._out_dir) + + locale_helpers = """\ +std::string GetCurrentLocale(JNIEnv* env) { + if (!env) { + __android_log_print(ANDROID_LOG_ERROR, + "cardboard_demo", + "GetCurrentLocale: JNIEnv is null"); + return "en"; + } + + const char* method = "toLanguageTag"; + jclass locale_class = env->FindClass("java/util/Locale"); + jmethodID get_default_method = env->GetStaticMethodID( + locale_class, "getDefault", "()Ljava/util/Locale;"); + jobject default_locale = + env->CallStaticObjectMethod(locale_class, get_default_method); + jmethodID get_method = + env->GetMethodID(locale_class, method, "()Ljava/lang/String;"); + if (get_method == nullptr) { + __android_log_print(ANDROID_LOG_ERROR, + "cardboard_demo", + "GetCurrentLocale: GetMethodID is null."); + return "en"; + } + + jobject get_str = env->CallObjectMethod(default_locale, get_method); + jstring get_str_jstring = static_cast(get_str); + + const char* data = env->GetStringUTFChars(get_str_jstring, nullptr); + size_t size = env->GetStringUTFLength(get_str_jstring); + std::string cpp_string(data, size); + env->ReleaseStringUTFChars(get_str_jstring, data); + + return cpp_string; +} +""" + locale_decls = """\ +const char* GetLocalizedString(StringId string_id, JNIEnv* env); +const char* GetLocalizedString(const std::string& key, JNIEnv* env);\ +""" + locale_defs = """\ +const char* GetLocalizedString(StringId string_id, JNIEnv* env) { + return GetLocalizedStringForLocale(kLocalizedStringTable[string_id], + GetCurrentLocale(env), env); +} + +const char* GetLocalizedString(const std::string& key, JNIEnv* env) { + return GetLocalizedStringForLocale(key, GetCurrentLocale(env), env); +} +""" + + cc_header = open( + os.path.join(self._out_dir, ('%s_strings.h' % self._namespace)), 'w') + cc_header.write("""\ +// GENERATED CODE : DO NOT EDIT BY HAND +#ifndef VR_APPS_CARDBOARD_DEMO_COMMON_TOOLS_%(namespace_upper)s_STRINGS_H_ +#define VR_APPS_CARDBOARD_DEMO_COMMON_TOOLS_%(namespace_upper)s_STRINGS_H_ + +#include +#include + +namespace %(namespace)s { + +typedef enum { +%(enums)s +} StringId; + +%(locale_decls)s +const char* GetLocalizedStringForLocale(StringId string_id, + const std::string& locale, + JNIEnv* env); +const char* GetLocalizedStringForLocale(const std::string& key, + const std::string& locale, + JNIEnv* env); + +} // namespace %(namespace)s + +#endif // VR_APPS_CARDBOARD_DEMO_COMMON_TOOLS_%(namespace_upper)s_STRINGS_H_ +""" % dict( + enums='\n'.join([' %s,' % self._GenerateEnumConstant(k) + for k in self._parsed_strings.keys()]), + namespace=self._namespace, + namespace_upper=self._namespace.upper(), + locale_decls=locale_decls)) + + cc_header.close() + + # TODO(bwuest): Use a template library like cheetah or jinja2 to make this + # generated code easier to read. + cc_source = open( + os.path.join(self._out_dir, ('%s_strings.cc' % self._namespace)), 'w') + cc_source.write( + """\ +// GENERATED CODE : DO NOT EDIT BY HAND +#include "%(namespace)s_strings.h" + +#include "../common/strings/localized_strings.h" + +#include +#include + +namespace { + +const char* const kLocalizedStringTable[] = { +%(parsed_strings)s +}; + + +%(locale_map_initialization_functions)s; + +void AddLocaleToMap(const std::string& locale, cardboard_demo::LocaleMap& locale_map) { +%(locale_map_initialization)s; +} + +// Retrieves the LocaleMap, lazily loading in the provided locale if necessary. +const cardboard_demo::LocaleMap& GetLocaleMapWithLocale(const std::string& locale) { + static cardboard_demo::LocaleMap s_locale_map; + // Lazily add the locale to the map if it's not already there. + if (s_locale_map.find(locale) == s_locale_map.end()) { + AddLocaleToMap(locale, s_locale_map); + } + + return s_locale_map; +} + +%(locale_helpers)s\ + +} // namespace + +namespace %(namespace)s { + +const char* GetLocalizedStringForLocale(StringId string_id, + const std::string& locale, + JNIEnv* env) { + return GetLocalizedStringForLocale(kLocalizedStringTable[string_id], locale, env); +} + +const char* GetLocalizedStringForLocale(const std::string& key, + const std::string& target_locale, + JNIEnv* env) { + if(!env) + { + __android_log_print(ANDROID_LOG_ERROR, + "cardboard_demo", + "GetLocalizedStringForLocale: JNIEnv is null"); + return nullptr; + } + + const std::vector locales = + cardboard_demo::GetProbableLocalesInOrder(target_locale); + for (auto& locale : locales) { + const cardboard_demo::LocaleMap& locale_map = GetLocaleMapWithLocale(locale); + auto locale_entry_iter = locale_map.find(locale); + if (locale_entry_iter != locale_map.end()) { + const char* out = + cardboard_demo::GetLocalizedStringFromMap(key, locale_entry_iter->second); + if (!out) { + continue; + } + return out; + } + } + return nullptr; +} + +%(locale_defs)s\ + +} // namespace %(namespace)s +""" % dict(namespace=self._namespace, + num_parsed_strings=len(self._parsed_strings), + locale_defs=locale_defs, + locale_helpers=locale_helpers, + locale_map_initialization_functions='\n'.join( + locale_map_initialization_functions), + locale_map_initialization='\n'.join(locale_map_initialization), + parsed_strings='\n'.join([ + ' "%s", // %s' % (k, v) + for k, v in self._parsed_strings.items() + ]))) + + cc_source.close() + + def _GenerateJava(self) -> None: + """Generates the Java output files, from the parsed input.""" + + # TODO(b/29054912): Refactor this method to be consistent with GenerateCc. + + if not os.path.exists(self._out_dir): + os.makedirs(self._out_dir) + strings_class = open( + os.path.join(self._out_dir, ('%s.java' % 'Strings')), 'w') + strings_class.write('package %s;\n\n' % self._namespace) + + strings_class.write('import java.util.HashMap;\n') + strings_class.write('import java.util.Locale;\n') + strings_class.write('import java.util.Map;\n') + strings_class.write('\n') + + strings_class.write('/** @hide */\n') + strings_class.write('public class %s {\n' % 'Strings') + # Generate constants for each string used as the 'stringId' which is passed + # to getStrings(String). + for key, value in self._parsed_strings.items(): + strings_class.write('public static final String %s = "%s";\n' % + (key, key)) + + strings_class.write('private static final ' + 'Map> LANGUAGE_MAP;\n') + strings_class.write('static {\n') + strings_class.write(' LANGUAGE_MAP = ' + 'new HashMap>();\n') + + locale_map = self._GetFullLocaleMap() + for locale_id in locale_map: + strings_class.write(' Map %s = ' + 'new HashMap();\n' % locale_id) + string_map = locale_map[locale_id] + for key, value in string_map.items(): + strings_class.write(' %s.put("%s", "%s");\n' % (locale_id, key, value)) + strings_class.write(' LANGUAGE_MAP.put("%s", %s);\n\n' % + (locale_id, locale_id)) + strings_class.write('}\n') + strings_class.write('\n') + strings_class.write('private static ' + 'Map getLanguageMap(Locale locale) {\n') + strings_class.write(' String language = locale.getLanguage();\n') + strings_class.write(' if (LANGUAGE_MAP.containsKey(language)) {\n') + strings_class.write(' return LANGUAGE_MAP.get(language);\n') + strings_class.write(' } else {\n') + strings_class.write(' // If language is not found, default to english.' + '\n') + strings_class.write(' return LANGUAGE_MAP.get("en");\n') + strings_class.write(' }\n') + strings_class.write('}\n') + strings_class.write('\n') + strings_class.write('private static String getString(' + 'String stringId, Locale locale) {\n') + strings_class.write(' Map stringMap = ' + 'getLanguageMap(locale);\n') + strings_class.write(' if (stringMap.containsKey(stringId)) {\n') + strings_class.write(' return stringMap.get(stringId);\n') + strings_class.write(' } else {\n') + strings_class.write(' // If the string is not found for the specific ' + 'language, fall back to the original string.\n') + strings_class.write(' return LANGUAGE_MAP.get("en").get(stringId);\n') + strings_class.write(' }\n') + strings_class.write('}\n') + strings_class.write('\n') + strings_class.write('public static String getString(String stringId) {\n') + strings_class.write(' return getString(stringId, Locale.getDefault());\n') + strings_class.write('}\n') + + strings_class.write('}\n') + + strings_class.close() + + def _ParseFile(self) -> None: + """Parses the file at self._resource_file_name into self._parsed_strings. + + The strings file contains the strings required by the application. + This file will consist of comment lines /* */ and strings lines: + "key" = "value";. + """ + self._parsed_strings = self._ParseStringsFile( + self._resource_file_name, self._check_substitution_tokens) + + def _ParseStringsFile(self, resource_file: str, + check_substitution_tokens: bool) -> Dict[str, str]: + """Parses the file at self._resource_file_name into self._parsed_strings. + + The strings file contains the strings required by the application. + This file will consist of comment lines /* */ and strings lines: + "key" = "value";. + + Args: + resource_file: The name of the Localizeable.strings file. + check_substitution_tokens: Whether or not to validate that the + substitution tokens are valid. + + Returns: + Map of key/value pairs of localized strings. + """ + parsed_strings = {} + strings_file = open(resource_file, 'r') + for line in strings_file: + match = re.match(r'"([a-zA-Z0-9_]*)"\s*=\s*"(.*)";$', line) + if match: + key = match.group(1) + value = match.group(2) + if check_substitution_tokens: + substitution_tokens = re.findall(r'(\%[0-9a-zA-Z@\.\$]+)', value) + if len(substitution_tokens) > 1: + for substitution_token in substitution_tokens: + # Match numbered substitutions ie. %1$d + if not re.match(r'\%[0-9]+\$', substitution_token): + sys.stderr.write( + 'Line: %s includes multiple substitution parameters, ' + 'some of which are not numbered. This can cause issues ' + 'in some languages. You are seeing this error because ' + '--check_substitution_tokens is passed to genstrings.py. ' + 'Remove this flag if you are not launched in languages ' + 'where this is a problem.' % line) + exit(1) + + parsed_strings[key] = value + strings_file.close() + return parsed_strings + + def _GetFullLocaleMap(self): + """Parses all the string files for each locale. + + The location of each locale is based on the location of + self._resource_file_name. + Returns: + Map of a map of key/value pairs of localized strings per locale. + """ + locale_map = {} + # Get the directory that the base strings file is in. + base_dir = os.path.abspath(os.path.join(self._resource_file_name, + os.pardir)) + # Get the root strings directory that contains all locales. + root_resource_dir = os.path.abspath(os.path.join(base_dir, os.pardir)) + # Get the base name of the strings file as that should be the same in each + # locale directory. + strings_filename = os.path.basename(self._resource_file_name) + # Get the list of all locale directories. + locale_dirs = os.listdir(root_resource_dir) + + # For each locale, parse the strings file and put that in a map. + for locale_dir in locale_dirs: + locale_file = os.path.abspath(os.path.join(root_resource_dir, locale_dir, + strings_filename)) + parsed_strings = self._ParseStringsFile(locale_file, + self._check_substitution_tokens) + # Strip off the lproj suffix so we are only left with the locale id. + locale_id = re.sub(r'\.lproj$', '', locale_dir) + locale_map[locale_id] = parsed_strings + return locale_map + + +def main(argv: List[str]) -> int: + """Main method.""" + try: + parser = optparse.OptionParser() + parser.add_option('--out_dir', + help='The directory where code should be placed', + dest='out_dir', + action='store', + default='') + parser.add_option( + '--resource_file', + help='The english Localizeable.strings file to be used', + dest='resource_file', + action='store', + default='../Resources/Strings/en.lproj/Localizable.string') + parser.add_option('--namespace', + help='Namespace used in generated code', + dest='namespace', + action='store', + default='sassafras') + parser.add_option('--check_substitution_tokens', + help=('When set, each string will be parsed for ' + 'substitution tokens %d, and will fail if there ' + 'are multiple substitution tokens and any are ' + 'not numbered.'), + dest='check_substitution_tokens', + action='store_true') + parser.add_option('--language', + help='Output language, either C++ (cc) or Java (java)', + dest='language', + action='store', + default='cc') + (options, args) = parser.parse_args(args=argv) + if args: + raise Error('No argument allowed.') + strings_parser = StringsParser(options.resource_file, options.namespace, + options.out_dir, + options.check_substitution_tokens, + options.language) + strings_parser.Run() + + return 0 + except Error as e: + sys.stderr.write('%s: Error: %s\n' % (os.path.basename(sys.argv[0]), + e.args[0])) + return 1 + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) diff --git a/paperscope/paperscope_launcher/common/tools/localization_strings.bzl b/paperscope/paperscope_launcher/common/tools/localization_strings.bzl new file mode 100644 index 00000000..d97a1a44 --- /dev/null +++ b/paperscope/paperscope_launcher/common/tools/localization_strings.bzl @@ -0,0 +1,80 @@ +load("//third_party/bazel_rules/rules_cc/cc:cc_library.bzl", "cc_library") + +def localization_strings_genrule( + name, + srcs, + visibility, + namespace, + resource_file): + """A genrule that creates C++ files for localized strings. + + This is a helper function that creates a genrule to generate localization + string files from a set of resource files. It is used by the + `localization_strings` macro. + + Args: + name: The name of the target. + srcs: A list of source files containing the localized strings. + visibility: The visibility of the generated files. + namespace: The namespace for the generated C++/Java/Objective-C code. + resource_file: The path to the primary resource file (e.g., + en.lproj/cardboard.strings). + """ + + native.genrule( + name = "gen_" + name, + outs = [ + namespace + "_strings.h", + namespace + "_strings.cc", + ], + srcs = srcs, + cmd = ("$(location //vr/apps/cardboard/demo/common/tools:genstrings)" + + " --resource_file=$(location %s)" % resource_file + + " --namespace=" + namespace + + " --out_dir=$(@D)" + + " --language=cc"), + tools = [ + "//vr/apps/cardboard/demo/common/tools:genstrings", + ], + visibility = visibility, + ) + native.filegroup( + name = "srcs_filegroup_" + name, + srcs = [ + namespace + "_strings.cc", + ], + visibility = visibility, + ) + native.filegroup( + name = "hdrs_filegroup_" + name, + srcs = [ + namespace + "_strings.h", + ], + visibility = visibility, + ) + +def localization_strings( + name, + srcs, + visibility, + namespace, + resource_file, + copts = None): + localization_strings_genrule( + name, + srcs, + visibility, + namespace, + resource_file, + ) + + cc_library( + name = name, + srcs = [":srcs_filegroup_" + name], + hdrs = [":hdrs_filegroup_" + name], + copts = copts, + visibility = visibility, + deps = [ + "//vr/apps/cardboard/demo/common/strings:localized_strings", + ], + ) diff --git a/paperscope/paperscope_launcher/common/tools/objc_file_set.bzl b/paperscope/paperscope_launcher/common/tools/objc_file_set.bzl new file mode 100644 index 00000000..44fff9bf --- /dev/null +++ b/paperscope/paperscope_launcher/common/tools/objc_file_set.bzl @@ -0,0 +1,59 @@ +"""Support for zipping a Fileset, retaining its directory structure.""" + +def _fileset_as_zip_impl(ctx): + """Implementation of the FilesetAsZip rule. + + Args: + ctx: The Skylark context. + Returns: + A list with the `DefaultInfo` provider that propagates a zip file for + each `Fileset` in `srcs`. + """ + srcs = ctx.files.srcs # Must be fileset rules. + outputs = [] + counter = 1 + + for fileset in srcs: + zipname = ctx.label.name + "." + str(counter) + ".zip" + counter += 1 + + output_zip = ctx.actions.declare_file(zipname) + outputs.append(output_zip) + + message = "Generating %s archive" % (output_zip) + sed_expr = "s|\\(%s/\\)\\(.*\\)|\\\\2=\\\\1\\\\2|g" % fileset.dirname + + # There's a //tools/zip with weird-ass args, but at least it's platform neutral. + command_expr = '%s c %s $(find -L %s | sed "%s")' % ( + ctx.executable._zip.path, + output_zip.path, + fileset.path, + sed_expr, + ) + ctx.actions.run_shell( + inputs = [ctx.executable._zip, fileset], + outputs = [output_zip], + command = command_expr, + mnemonic = "EXPFilesetAsZipImpl", + progress_message = "%s %s" % (message, ctx.label), + ) + return [DefaultInfo(files = depset(outputs))] + +fileset_as_zip = rule( + implementation = _fileset_as_zip_impl, + attrs = { + "srcs": attr.label_list( + allow_files = True, + mandatory = True, + ), + "_zip": attr.label( + default = Label("//tools/zip:zipper"), + cfg = "exec", + allow_single_file = True, + executable = True, + ), + }, +) + +def FilesetAsZip(name, srcs): + fileset_as_zip(name = name, srcs = srcs) diff --git a/paperscope/paperscope_launcher/resources/strings/ar.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/ar.lproj/cardboard.strings new file mode 100644 index 00000000..b7815415 --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/ar.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "برنامج تعليمي"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "فيديو الترحيب"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "المستكشف"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "معرض"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "تنزه في المناطق الحضرية"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "كليدوسكوب"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "رحلة المنطقة القطبية الشمالية"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "تعذر الاتصال بالخادم\nتحقق من الاتصال بالإنترنت"; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "عذرًا، حدث شيئ ما خاطئ، يُرجى إعادة المحاولة لاحقًا"; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "اضغط على الزر\nللنقر"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "الإمالة للوصول إلى\nالصفحة الرئيسية"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "‏Hawk Rattle\n© راندي كوك"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "‏Vatican Mask\n© راندي كوك"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "‏Chief's Mask\n© راندي كوك"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "‏The Visionary\n© راندي كوك"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "‏Beethoven\n© راندي كوك"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "نموذج استكشافات\nالمتحف الأمريكي للتاريخ الطبيعي"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "متحف فرونتيرز أوف فلايت"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "متحف مكوك الفضاء انديفور"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "كويفا دل إنديو\nبورتوريكو"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "ينابيع جنهافر الساخنة\nأيسلندا"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "مسبار المريخ المتجوّل\nصورة من ناسا/مختبر الدفع النفاث"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(انقر للانتقال إلى %2$s)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "باریس"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "طوکیو"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "البندقية"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "نيويورك"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "روما"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "القدس"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "مونت كارلو"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "لندن"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "الحيد المرجاني العظيم"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "دليل الجولة"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "مقاطع الفيديو التابعة لي"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "الخروج"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "الرجوع إلى العروض"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "يمكنك تدوير هاتفك للعودة إلى القائمة"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "اسحب هاتفك من العارض للخروج من العروض التوضيحية."; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "التالي"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "اضغط على الزر للتحديد."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "يمكنك تدوير العارض للتنقل إلى الخلف."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "جرِّبه الآن."; diff --git a/paperscope/paperscope_launcher/resources/strings/bg.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/bg.lproj/cardboard.strings new file mode 100644 index 00000000..c20055dd --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/bg.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "Урок"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "Приветствен видеоклип"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "Изследовател"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "Изложба"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "Градска разходка"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "Калейдоскоп"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "Пътешествие в Арктика"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "Не може да се осъществи връзка със сървъра\nПроверете връзката си с интернет"; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "За съжаление, нещо се обърка. Моля, опитайте отново по-късно."; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "Натиснете бутона\nза кликване"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "Наклонете\nза връщане в началото"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Дрънкалка сокол\n© Ранди Кук"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Ватиканска маска\n© Ранди Кук"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Маска на вожд\n© Ранди Кук"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "Прорицателят\n© Ранди Кук"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Бетховен\n© Ранди Кук"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "Американски музей по естествена история\nПримерни експедиции"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "Границите на полета"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "Космическата совалка „Индевър“"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "Куева дел Индио\nПуерто Рико"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "Горещи извори в Гюнюхвер\nИсландия"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "Марсоходът „Спирит“\nИзображение от NASA/JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(Кликнете, за да преминете към %2$s)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "Париж"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "Токио"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "Венеция"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "Ню Йорк"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "Рим"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "Ерусалим"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "Монте Карло"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "Лондон"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "Големият бариерен риф"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "Екскурзовод"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "Моите видеоклипове"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "ИЗХОД"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "КЪМ ДЕМОНСТРАЦИИТЕ"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "Завъртете телефона си, за да се върнете в менюто"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "Извадете телефона си от очилата, за да излезете от демонстрациите"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "НАПРЕД"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "Натиснете бутона, за да изберете."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "Завъртете очилата, за да навигирате назад."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "Изпробвайте сега."; diff --git a/paperscope/paperscope_launcher/resources/strings/ca.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/ca.lproj/cardboard.strings new file mode 100644 index 00000000..87c72ca3 --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/ca.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "Tutorial"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "Vídeo de benvinguda"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "Explorador"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "Exposició"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "Excursió per la ciutat"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "Calidoscopi"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "Viatge per l'Àrtic"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "No es pot connectar al servidor\nComprova la connexió a Internet"; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "S'ha produït un error. Torna a provar-ho més tard."; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "Fes clic\nal botó"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "Inclina el visor\nper tornar a l'inici"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Hawk Rattle\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Vatican Mask\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Chief's Mask\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "The Visionary\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "Museu nord-americà d'història natural (American Museum of Natural History)\nMostra d'expedicions"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "Museu aeroespacial Frontiers of Flight"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "Transbordador espacial Endeavour"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "Cueva del Indio\nPuerto Rico"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "Fonts termals de Gunnuhver\nIslàndia"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "Spirit, el robot de Mart\nImatge de la NASA/JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(Fes clic per anar a %2$s)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "París"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "Tòquio"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "Venècia"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "Nova York"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "Roma"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "Jerusalem"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "Montecarlo"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "Londres"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "Gran Barrera de Corall"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "Visita guiada"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "Els meus vídeos"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "SURT"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "TORNA A DEMOS"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "Gira el telèfon per tornar al menú"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "Treu el telèfon del visor per sortir de les demostracions"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "SEGÜENT"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "Prem el botó per fer una selecció."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "Gira el visor per tornar enrere."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "Prova-ho ara."; diff --git a/paperscope/paperscope_launcher/resources/strings/cs.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/cs.lproj/cardboard.strings new file mode 100644 index 00000000..3d4e8bf1 --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/cs.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "Výukový program"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "Uvítací video"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "Explorer"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "Exhibit"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "Urban Hike"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "Kaleidoscope"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "Arktická výprava"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "Nelze se připojit k serveru,\nzkontrolujte připojení k internetu."; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "Omlouváme se, něco se pokazilo, zkuste to prosím znovu později."; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "Zmáčknutím tlačítka\nkliknete"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "Nakloněním se vrátíte\ndomů"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Hawk Rattle\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Vatican Mask\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Chief's Mask\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "The Visionary\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "Americké přírodovědné muzeum\nUkázka expedic"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "Muzeum Frontiers of Flight"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "Raketoplán Endeavour"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "Cueva del Indio\nPortoriko"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "Termální prameny Gunnuhver\nIsland"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "Vozítko Spirit na Marsu\nObrázek od NASA/JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(Kliknutím přejdete na místo: %2$s)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "Paříž"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "Tokio"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "Benátky"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "New York"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "Řím"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "Jeruzalém"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "Monte Carlo"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "Londýn"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "Velký bariérový útes"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "Tour Guide"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "Moje videa"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "UKONČIT"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "ZPĚT NA UKÁZKY"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "Zpět do nabídky se vrátíte otočením telefonu"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "Pokud chcete ukončit ukázky, vyjměte telefon z brýlí"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "DALŠÍ"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "Výběr provedete stisknutím tlačítka."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "Zpět se dostanete otočením brýlí."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "Zkuste si to."; diff --git a/paperscope/paperscope_launcher/resources/strings/da.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/da.lproj/cardboard.strings new file mode 100644 index 00000000..4318f814 --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/da.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "Selvstudie"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "Velkomstvideo"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "Explorer"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "Exhibit"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "Urban Hike"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "Kaleidoscope"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "Polarrejse"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "Der kan ikke oprettes forbindelse til serveren\nKontrollér din internetforbindelse"; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "Der gik noget galt. Prøv igen senere"; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "Tryk på knappen\nfor at klikke"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "Vip for at komme til\nstartsiden"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Hawk Rattle\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Vatican Mask\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Chief's Mask\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "The Visionary\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "American Museum of Natural History\nSmagsprøve på Expeditions"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "Frontiers of Flight"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "Rumfærgen Endeavour"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "Cueva del Indio\nPuerto Rico"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "Varme kilder ved Gunnuhver\nIsland"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "Mars Spirit Rover\nBillede fra NASA/JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(Klik for at gå til %2$s)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "Paris"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "Tokyo"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "Venedig"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "New York"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "Rom"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "Jerusalem"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "Monte Carlo"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "London"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "Great Barrier Reef"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "Tour Guide"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "My Videos"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "AFSLUT"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "TILBAGE TIL DEMOER"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "Roter telefonen for at komme tilbage til menuen"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "Tag din telefon ud af brillen for at afslutte demoerne"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "NÆSTE"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "Tryk på knappen for at vælge."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "Drej din fremviser for at gå tilbage."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "Prøv det nu."; diff --git a/paperscope/paperscope_launcher/resources/strings/de.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/de.lproj/cardboard.strings new file mode 100644 index 00000000..bd809eeb --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/de.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "Anleitung"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "Einführungsvideo"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "Explorer"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "Ausstellung"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "Stadttour"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "Kaleidoskop"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "Reise in die Arktis"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "Keine Verbindung zum Server möglich\nBitte überprüfe deine Internetverbindung."; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "Ein Fehler ist aufgetreten. Bitte versuche es später noch einmal."; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "Zum Auslösen\nTaste drücken"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "Kippen, um zur\nStartseite zu wechseln"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Hawk Rattle\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Vatican Mask\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Chief's Mask\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "The Visionary\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "American Museum of Natural History\nExpeditionsbeispiel"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "Flugpioniere"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "Raumfähre \"Endeavour\""; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "Cueva del Indio\nPuerto Rico"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "Heiße Quellen von Gunnuhver\nIsland"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "Mars-Rover \"Spirit\"\nBild von NASA/JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(Klicken, um zu \"%2$s\" zu gehen)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "Paris"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "Tokio"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "Venedig"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "New York"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "Rom"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "Jerusalem"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "Monte Carlo"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "London"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "Great Barrier Reef"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Google Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "Reiseführer"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "Meine Videos"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "BEENDEN"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "Zurück zu Demos"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "Drehe dein Smartphone, um zum Menü zurückzukehren."; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "Trenne das Smartphone vom Gerät, um die Demos zu beenden."; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "WEITER"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "Um eine Schaltfläche zu betätigen, drücke die Taste an deinem Gerät."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "Um zurückzunavigieren, drehe die Brille."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "Probiere es jetzt aus."; diff --git a/paperscope/paperscope_launcher/resources/strings/el.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/el.lproj/cardboard.strings new file mode 100644 index 00000000..d4b05f20 --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/el.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "Οδηγός εκμάθησης"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "Βίντεο καλωσορίσματος"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "Εξερευνητής"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "Έκθεμα"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "Περίπατος στην πόλη"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "Καλειδοσκόπιο"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "Ταξίδι στην Αρκτική"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "Αδύνατη η σύνδεση στο διακομιστή:\nελέγξτε τη σύνδεσή σας στο διαδίκτυο."; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "Παρουσιάστηκε κάποιο πρόβλημα. Δοκιμάστε ξανά αργότερα."; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "Πατήστε το κουμπί\nγια να κάνετε κλικ"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "Δώστε κλίση για επιστροφή\nστην αρχή"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Hawk Rattle\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Vatican Mask\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Chief's Mask\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "The Visionary\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "Αμερικανικό Μουσείο Φυσικής Ιστορίας\nΔείγμα από τις Αποστολές"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "Τα σύνορα της πτήσης"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "Διαστημικό λεωφορείο \"Endeavour\""; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "Cueva del Indio\nΠόρτο Ρίκο"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "Θερμές πηγές Gunnuhver\nΙσλανδία"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "Mars Spirit Rover\nΕικόνα από NASA/JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(Κλικ για μετάβαση σε %2$s)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "Παρίσι"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "Τόκιο"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "Βενετία"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "Νέα Υόρκη"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "Ρώμη"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "Ιερουσαλήμ"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "Μόντε Κάρλο"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "Λονδίνο"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "Μεγάλος Κοραλλιογενής Ύφαλος"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "Ταξιδιωτικός οδηγός"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "Τα βίντεό μου"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "ΕΞΟΔΟΣ"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "ΕΠΙΣΤΡΟΦΗ ΣΤΑ DEMO"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "Περιστρέψτε το τηλέφωνό σας για να επιστρέψετε στο μενού"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "Αφαιρέστε το τηλέφωνό σας από το σύστημα προβολής για έξοδο από τις επιδείξεις"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "ΕΠΟΜΕΝΟ"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "Πατήστε το κουμπί για να επιλέξετε."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "Περιστρέψτε το σύστημα προβολής που διαθέτετε, για να περιηγηθείτε προς τα πίσω."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "Δοκιμάστε το τώρα."; diff --git a/paperscope/paperscope_launcher/resources/strings/en.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/en.lproj/cardboard.strings new file mode 100644 index 00000000..331a2ab2 --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/en.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "Tutorial"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "Welcome Video"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "Explorer"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "Exhibit"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "Urban Hike"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "Kaleidoscope"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "Arctic Journey"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "Cannot connect to server\ncheck your internet connection"; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "Sorry something went wrong, please try again later"; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "Press the button\nto click"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "Tilt to get\nhome"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Hawk Rattle\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Vatican Mask\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Chief's Mask\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "The Visionary\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "American Museum of Natural History\nExpeditions Sample"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "Frontiers of Flight"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "Endeavour Space Shuttle"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "Cueva del Indio\nPuerto Rico"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "Gunnuhver Hot Springs\nIceland"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "Mars Spirit Rover\nImage from NASA/JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(Click to go to %2$s)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "Paris"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "Tokyo"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "Venice"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "New York"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "Rome"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "Jerusalem"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "Monte Carlo"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "London"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "Great Barrier Reef"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "Tour Guide"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "My Videos"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "EXIT"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "BACK TO DEMOS"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "Rotate your phone to get back to the menu"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "Take your phone out of your viewer to exit the demos"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "NEXT"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "Press the button to select."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "Rotate your viewer in order to navigate back."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "Try it out now."; diff --git a/paperscope/paperscope_launcher/resources/strings/en_AU.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/en_AU.lproj/cardboard.strings new file mode 100644 index 00000000..abdf277c --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/en_AU.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "Tutorial"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "Welcome Video"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "Explorer"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "Exhibit"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "Urban Hike"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "Kaleidoscope"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "Arctic Journey"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "Cannot connect to server\nCheck your internet connection"; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "Sorry, something went wrong. Please try again later."; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "Press the button\nto click"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "Tilt to get\nhome"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Hawk Rattle\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Vatican Mask\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Chief's Mask\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "The Visionary\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "American Museum of Natural History\nExpeditions sample"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "Frontiers of Flight"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "Endeavour Space Shuttle"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "Cueva del Indio\nPuerto Rico"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "Gunnuhver Hot Springs\nIceland"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "Mars Spirit Rover\nImage from NASA/JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(Click to go to %2$s)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "Paris"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "Tokyo"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "Venice"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "New York"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "Rome"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "Jerusalem"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "Monte Carlo"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "London"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "Great Barrier Reef"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "Tour Guide"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "My videos"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "EXIT"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "BACK TO DEMOS"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "Rotate your phone to get back to the menu"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "Take your phone out of your viewer to exit the demos"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "NEXT"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "Press the button to select."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "Rotate your viewer in order to navigate back."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "Try it out now."; diff --git a/paperscope/paperscope_launcher/resources/strings/en_GB.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/en_GB.lproj/cardboard.strings new file mode 100644 index 00000000..abdf277c --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/en_GB.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "Tutorial"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "Welcome Video"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "Explorer"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "Exhibit"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "Urban Hike"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "Kaleidoscope"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "Arctic Journey"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "Cannot connect to server\nCheck your internet connection"; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "Sorry, something went wrong. Please try again later."; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "Press the button\nto click"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "Tilt to get\nhome"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Hawk Rattle\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Vatican Mask\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Chief's Mask\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "The Visionary\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "American Museum of Natural History\nExpeditions sample"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "Frontiers of Flight"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "Endeavour Space Shuttle"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "Cueva del Indio\nPuerto Rico"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "Gunnuhver Hot Springs\nIceland"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "Mars Spirit Rover\nImage from NASA/JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(Click to go to %2$s)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "Paris"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "Tokyo"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "Venice"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "New York"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "Rome"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "Jerusalem"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "Monte Carlo"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "London"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "Great Barrier Reef"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "Tour Guide"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "My videos"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "EXIT"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "BACK TO DEMOS"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "Rotate your phone to get back to the menu"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "Take your phone out of your viewer to exit the demos"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "NEXT"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "Press the button to select."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "Rotate your viewer in order to navigate back."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "Try it out now."; diff --git a/paperscope/paperscope_launcher/resources/strings/en_IN.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/en_IN.lproj/cardboard.strings new file mode 100644 index 00000000..abdf277c --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/en_IN.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "Tutorial"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "Welcome Video"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "Explorer"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "Exhibit"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "Urban Hike"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "Kaleidoscope"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "Arctic Journey"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "Cannot connect to server\nCheck your internet connection"; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "Sorry, something went wrong. Please try again later."; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "Press the button\nto click"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "Tilt to get\nhome"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Hawk Rattle\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Vatican Mask\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Chief's Mask\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "The Visionary\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "American Museum of Natural History\nExpeditions sample"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "Frontiers of Flight"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "Endeavour Space Shuttle"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "Cueva del Indio\nPuerto Rico"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "Gunnuhver Hot Springs\nIceland"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "Mars Spirit Rover\nImage from NASA/JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(Click to go to %2$s)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "Paris"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "Tokyo"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "Venice"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "New York"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "Rome"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "Jerusalem"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "Monte Carlo"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "London"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "Great Barrier Reef"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "Tour Guide"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "My videos"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "EXIT"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "BACK TO DEMOS"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "Rotate your phone to get back to the menu"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "Take your phone out of your viewer to exit the demos"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "NEXT"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "Press the button to select."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "Rotate your viewer in order to navigate back."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "Try it out now."; diff --git a/paperscope/paperscope_launcher/resources/strings/es.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/es.lproj/cardboard.strings new file mode 100644 index 00000000..700e42ad --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/es.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "Tutorial"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "Vídeo de bienvenida"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "Explorador"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "Exposición"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "Paseo por la ciudad"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "Caleidoscopio"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "Viaje ártico"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "No se puede conectar con el servidor\nComprueba tu conexión a Internet"; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "Se ha producido un error; vuelve a intentarlo dentro de un rato"; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "Pulsa el botón\npara hacer clic"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "Inclínalo para ir\nal inicio"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Sonajero de halcón\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Máscara vaticana\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Máscara del jefe\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "El vidente\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "Museo Americano de Ciencias Naturales (Nueva York)\nMuestra de Expediciones"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "Fronteras del vuelo"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "Lanzadera espacial \"Endeavour\""; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "Cueva del Indio\nPuerto Rico"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "Manantiales de aguas termales de Gunnuhver\nIslandia"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "Rover \"Spirit\" de Marte\nImagen de la NASA/JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(Haz clic para ir a %2$s)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "París"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "Tokio"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "Venecia"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "Nueva York"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "Roma"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "Jerusalén"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "Montecarlo"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "Londres"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "La Gran Barrera de Coral"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "Visita guiada"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "Mis vídeos"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "SALIR"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "VOLVER A LAS DEMOS"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "Gira tu teléfono para volver al menú"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "Saca tu teléfono del visor para salir de las demos"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "SIGUIENTE"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "Pulsa el botón para seleccionar."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "Para ir a la imagen anterior, gira el visor."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "Pruébalo ahora."; diff --git a/paperscope/paperscope_launcher/resources/strings/es_MX.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/es_MX.lproj/cardboard.strings new file mode 100644 index 00000000..55db7d0f --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/es_MX.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "Instructivo"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "Video de bienvenida"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "Explorador"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "Exhibición"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "Caminata urbana"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "Caleidoscopio"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "Viaje al Ártico"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "No se puede establecer conexión con el servidor\nComprueba la conexión a Internet"; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "Se produjo un error. Vuelve a intentarlo más tarde."; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "Presiona el botón\npara hacer clic"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "Inclina el visor\npara ir al inicio"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Hawk Rattle\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Vatican Mask\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Chief's Mask\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "The Visionary\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "Museo de historia natural de los Estados Unidos\nMuestra de expediciones"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "Frontiers of Flight"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "Transbordador espacial Endeavour"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "Cueva del Indio\nPuerto Rico"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "Fuente termal de Gunnuhver\nIslandia"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "Spirit, robot explorador de Marte\nImagen de NASA/JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(Haz clic para ir a %2$s)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "París"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "Tokio"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "Venecia"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "Nueva York"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "Roma"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "Jerusalén"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "Monte Carlo"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "Londres"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "Gran Barrera de Coral"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "Guía turística"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "Mis videos"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "SALIR"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "VOLVER A LAS DEMOS"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "Gira el teléfono para volver al menú"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "Quita el teléfono del visor para salir de las demostraciones"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "SIGUIENTE"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "Presiona el botón para seleccionar."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "Gira el visor para navegar hacia atrás."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "Pruébalo ahora"; diff --git a/paperscope/paperscope_launcher/resources/strings/es_US.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/es_US.lproj/cardboard.strings new file mode 100644 index 00000000..55db7d0f --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/es_US.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "Instructivo"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "Video de bienvenida"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "Explorador"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "Exhibición"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "Caminata urbana"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "Caleidoscopio"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "Viaje al Ártico"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "No se puede establecer conexión con el servidor\nComprueba la conexión a Internet"; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "Se produjo un error. Vuelve a intentarlo más tarde."; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "Presiona el botón\npara hacer clic"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "Inclina el visor\npara ir al inicio"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Hawk Rattle\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Vatican Mask\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Chief's Mask\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "The Visionary\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "Museo de historia natural de los Estados Unidos\nMuestra de expediciones"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "Frontiers of Flight"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "Transbordador espacial Endeavour"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "Cueva del Indio\nPuerto Rico"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "Fuente termal de Gunnuhver\nIslandia"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "Spirit, robot explorador de Marte\nImagen de NASA/JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(Haz clic para ir a %2$s)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "París"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "Tokio"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "Venecia"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "Nueva York"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "Roma"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "Jerusalén"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "Monte Carlo"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "Londres"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "Gran Barrera de Coral"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "Guía turística"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "Mis videos"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "SALIR"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "VOLVER A LAS DEMOS"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "Gira el teléfono para volver al menú"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "Quita el teléfono del visor para salir de las demostraciones"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "SIGUIENTE"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "Presiona el botón para seleccionar."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "Gira el visor para navegar hacia atrás."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "Pruébalo ahora"; diff --git a/paperscope/paperscope_launcher/resources/strings/fa.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/fa.lproj/cardboard.strings new file mode 100644 index 00000000..58ace530 --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/fa.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "آموزش گام به گام"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "ویدیوی خوش آمدگویی"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "کاوش‌گر"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "نمایش"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "راه‌پیمایی در شهر"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "زیبابین"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "سفر به قطب شمال"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "به سرور متصل نشدید\nاتصال اینترنت را بررسی کنید"; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "متأسفم... مشکلی روی داد، لطفاً بعداً دوباره امتحان کنید"; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "دکمه را\nبرای کلیک کردن فشار دهید"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "کج کنید\nتا به صفحه اصلی بروید"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Hawk Rattle\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Vatican Mask\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Chief's Mask\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "Visionary\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "موزه تاریخ طبیعی آمریکا\nنمونه اکتشاف"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "مرزهای پرواز"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "شاتل فضایی اندور"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "کیووا دل ایندیو\nپورتو ریکو"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "بهار گرم گونوهور\nایسلند"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "‏مارس اسپیریت روور\nتصویر از NASA/JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(برای رفتن به %2$s کلیک کنید)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "پاریس"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "توکیو"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "ونیز"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "نیویورک"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "رم"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "بیت المقدس"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "مونته کارلو"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "لندن"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "صخره گریت بریر"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "راهنمای تور"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "ویدیوهای من"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "خروج"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "بازگشت به بخش‌های نمایشی"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "تلفنتان را بچرخانید تا به منو برگردید"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "برای خروج از نسخه‌های نمایشی، تلفنتان را از نظاره‌گر بیرون ببرید"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "بعدی"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "دکمه را برای انتخاب فشار دهید."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "برای پیمایش به عقب، نظاره‌گر را بچرخانید."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "الان امتحان کنید."; diff --git a/paperscope/paperscope_launcher/resources/strings/fi.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/fi.lproj/cardboard.strings new file mode 100644 index 00000000..f3436d29 --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/fi.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "Ohje"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "Tervehdysvideo"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "Tutkimusmatkailija"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "Näyttely"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "Kaupunkikävely"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "Kaleidoskooppi"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "Arktinen matka"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "Palvelimeen ei saada yhteyttä.\nTarkista internetyhteys."; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "Jokin meni pieleen. Yritä myöhemmin uudelleen."; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "Klikkaa painamalla\npainiketta."; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "Siirry etusivulle\nkallistamalla."; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Hawk Rattle\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Vatican Mask\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Chief's Mask\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "The Visionary\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "American Museum of Natural History\nTutkimusmatkailija-katkelma"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "Frontiers of Flight"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "Endeavour-avaruusalus"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "Cueva del Indio\nPuerto Rico"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "Gunnuhverin kuumat lähteet\nIslanti"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "Mars-mönkijä Spirit\nKuvalähde: NASA/JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(Siirry sijaintiin %2$s klikkaamalla.)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "Pariisi"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "Tokio"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "Venetsia"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "New York"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "Rooma"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "Jerusalem"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "Monte Carlo"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "Lontoo"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "Iso valliriutta"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "Matkaopas"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "Omat videot"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "POISTU"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "TAKAISIN DEMOIHIN"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "Palaa valikkoon kääntämällä puhelinta."; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "Poistu esittelystä ottamalla puhelin pois laseista."; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "SEURAAVA"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "Valitse painamalla painiketta."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "Siirry takaisin kääntämällä laseja."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "Kokeile nyt."; diff --git a/paperscope/paperscope_launcher/resources/strings/fil.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/fil.lproj/cardboard.strings new file mode 100644 index 00000000..09a3b0fd --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/fil.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "Tutorial"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "Welcome Video"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "Explorer"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "Exhibit"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "Urban Hike"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "Kaleidoscope"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "Paglalakbay sa Arctic"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "Hindi makakonekta sa server\ntingnan ang iyong koneksyon sa internet"; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "Paumanhin nagkaroon ng problema, pakisubukang muli sa ibang pagkakataon"; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "Pindutin ang button\nupang i-click"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "I-tilt upang pumunta sa\nhome"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Hawk Rattle\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Vatican Mask\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Chief's Mask\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "The Visionary\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "American Museum of Natural History\nSample ng Mga Expedition"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "Frontiers of Flight"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "Endeavour Space Shuttle"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "Cueva del Indio\nPuerto Rico"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "Gunnuhver Hot Springs\nIceland"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "Mars Spirit Rover\nImahe mula sa NASA/JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(I-click upang pumunta sa %2$s)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "Paris"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "Tokyo"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "Venice"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "New York"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "Rome"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "Jerusalem"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "Monte Carlo"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "London"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "Great Barrier Reef"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "Gabay sa Paglilibot"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "Aking Mga Video"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "MAG-EXIT"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "BALIK SA MGA DEMO"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "I-rotate ang telepono mo upang bumalik sa menu"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "Lumabas sa iyong viewer ng telepono mo para lumabas sa mga demo"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "SUSUNOD"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "Pindutin ang button upang piliin."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "I-rotate ang iyong viewer upang mag-navigate pabalik."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "Subukan ito ngayon."; diff --git a/paperscope/paperscope_launcher/resources/strings/fr.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/fr.lproj/cardboard.strings new file mode 100644 index 00000000..d8e0766c --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/fr.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "Didacticiel"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "Vidéo de bienvenue"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "Explorateur"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "Exposition"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "Randonnée urbaine"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "Kaléidoscope"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "Voyage en Arctique"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "Impossible de se connecter au serveur.\nVérifiez votre connexion Internet."; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "Une erreur s'est produite. Veuillez réessayer plus tard."; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "Appuyez sur le bouton\npour sélectionner."; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "Inclinez Cardboard pour revenir\nà la page d'accueil."; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Hawk Rattle\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Vatican Mask\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Chief's Mask\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "The Visionary\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "Muséum américain d'histoire naturelle\nExemple d'utilisation d'Expéditions"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "Musée Frontiers of Flight"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "Navette spatiale Endeavour"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "Grotte de l'Indien\nPorto Rico"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "Sources thermales de Gunnuhver\nIslande"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "Mars Spirit Rover\nImage de la NASA/JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(cliquez pour accéder à %2$s)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "Paris"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "Tokyo"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "Venise"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "New York"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "Rome"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "Jérusalem"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "Monte Carlo"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "Londres"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "Grande Barrière de corail"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "Visite guidée"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "Mes vidéos"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "QUITTER"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "RETOUR AUX DÉMOS"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "Faites pivoter votre téléphone pour revenir au menu."; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "Pour quitter les démos, sortez votre téléphone de la visionneuse."; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "SUIVANT"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "Appuyez sur le bouton pour sélectionner."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "Pivotez votre visionneuse pour revenir à la page précédente."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "Essayez maintenant."; diff --git a/paperscope/paperscope_launcher/resources/strings/he.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/he.lproj/cardboard.strings new file mode 100644 index 00000000..97421100 --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/he.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "מדריך"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "סרטון היכרות"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "סייר"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "הצגה"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "טיול רגלי בעיר"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "קליידוסקופ"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "מסע בקוטב הצפוני"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "לא ניתן להתחבר לשרת\nבדוק את החיבור לאינטרנט"; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "מצטערים, משהו השתבש. נסה שוב מאוחר יותר"; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "לחץ על הכפתור\nכדי להפעיל"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "הטה כדי לעבור\nלמסך הראשי"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Hawk Rattle\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Vatican Mask\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Chief's Mask\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "The Visionary\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "המוזיאון האמריקאי לתולדות הטבע\nדוגמה למסע"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "‏מוזיאון Frontiers of Flight"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "מעבורת החלל אנדוור"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "‏מערת Cueva del Indio\nפוארטו ריקו"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "‏מעיינות חמים ב-Gunnuhver\nאיסלנד"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "‏רכב המאדים 'ספיריט'\nתמונה מאת NASA/JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(לחץ כדי לעבור אל %2$s)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "פריס"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "טוקיו"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "ונציה"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "ניו יורק"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "רומא"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "ירושלים"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "מונטה קרלו"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "לונדון"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "שונית המחסום הגדולה"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "מדריך טיולים"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "הסרטונים שלי"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "יציאה"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "חזרה להדגמות"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "סובב את הטלפון כדי לחזור לתפריט"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "כדי לצאת מתצוגת ההדגמה צריך להוציא את הטלפון ממכשיר הצפייה"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "הבא"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "לחץ על הלחצן כדי לבחור."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "סובב את מכשיר הצפייה כדי לנווט חזרה."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "נסה זאת עכשיו."; diff --git a/paperscope/paperscope_launcher/resources/strings/hi.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/hi.lproj/cardboard.strings new file mode 100644 index 00000000..02ec1b00 --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/hi.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "ट्यूटोरियल"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "स्वागत वीडियो"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "अन्वेषक"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "प्रदर्शन"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "अर्बन हाइक"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "कलाइडोस्कोप"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "आर्कटिक की यात्रा"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "सर्वर से कनेक्‍ट नहीं हो सकता\nअपना इंटरनेट कनेक्शन जांचें"; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "क्षमा करें कुछ गलत हुआ, कृपया बाद में फिर से प्रयास करें"; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "क्लिक करने के लिए\nबटन दबाएं"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "होम पर जाने\nके लिए झुकाएं"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Hawk Rattle\n© रैंड कुक"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Vatican Mask\n© रैंड कुक"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Chief's Mask\n© रैंड कुक"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "The Visionary\n© रैंड कुक"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Beethoven\n© रैंड कुक"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "अमेरिकन म्यूज़ियम ऑफ़ नेचुरल हिस्ट्री\nखोजयात्रा नमूना"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "फ़्रंटियर्स ऑफ़ फ़्लाइट"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "एंडेवर अंतरिक्ष शटल"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "क्यूवा डेल इंडियो\nपोर्टो रिको"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "गुनह्वेर हॉट स्प्रिंग्स\nआइसलैंड"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "मार्स स्पिरिट रोवर\nNASA/JPL से चित्र"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(%2$s पर जाने के लिए क्लिक करें)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "पेरिस"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "टोक्यो"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "वेनिस"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "न्यूयॉर्क"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "रोम"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "यरूशलम"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "मोंटे कार्लो"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "लंदन"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "ग्रेट बैरियर रीफ़"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "पृथ्वी"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "भ्रमण मार्गदर्शक"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "मेरे वीडियो"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "बाहर आएं"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "डेमो पर वापस जाएं"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "मेनू पर वापस जाने के लिए अपना फ़ोन घुमाएं"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "अगर आपको डेमो नहीं देखने हैं, तो अपने फ़ोन को व्यूअर से बाहर निकालें"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "अगला"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "चुनने के लिए बटन दबाएं."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "पीछे नेविगेट करने के लिए अपने व्यूअर को घुमाएं."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "इसे अभी आज़माएं."; diff --git a/paperscope/paperscope_launcher/resources/strings/hr.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/hr.lproj/cardboard.strings new file mode 100644 index 00000000..8e352b78 --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/hr.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "Vodič"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "Videozapis dobrodošlice"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "Istraživač"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "Izložak"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "Urbano pješačenje"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "Kaleidoskop"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "Arktičko putovanje"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "Povezivanje s poslužiteljem nije moguće\nprovjerite internetsku vezu"; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "Nešto nije u redu; pokušajte ponovno kasnije"; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "Da biste kliknuli, pritisnite tipku \n"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "Nagnite da biste stigli\nkući"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Hawk Rattle\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Vatikanska maska\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Maska poglavice\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "Vidjelica\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "Američki prirodoslovni muzej\nPrimjer ekspedicija"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "Muzej Frontiers of Flight"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "Svemirska letjelica Endeavour"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "Cueva del Indio\nPortoriko"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "Vrelo Gunnuhver\nIsland"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "Rover Spirit na Marsu\nSliku omogućuju NASA/JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(Kliknite da biste prešli na %2$s)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "Pariz"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "Tokio"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "Venecija"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "New York"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "Rim"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "Jeruzalem"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "Monte Carlo"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "London"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "Veliki koraljni greben"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "Vodič kroz aplikaciju"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "Moji videozapisi"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "IZLAZ"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "POVRATAK NA POKAZNE APLIKACIJE"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "Okrenite telefon da biste se vratili na izbornik"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "Izvadite telefon iz maske za izlazak iz pokaznih verzija"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "DALJE"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "Pritisnite gumb za odabir."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "Zakrenite svoju masku kako biste se mogli vratiti natrag."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "Isprobajte odmah."; diff --git a/paperscope/paperscope_launcher/resources/strings/hu.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/hu.lproj/cardboard.strings new file mode 100644 index 00000000..568cb8bb --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/hu.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "Útmutató"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "Üdvözlő videó"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "Explorer"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "Exhibit"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "Urban Hike"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "Kaleidoscope"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "Kirándulás a sarkkörön túlra"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "Nem sikerült csatlakozni a szerverre.\nEllenőrizze internetkapcsolatát."; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "Sajnos hiba történt, próbálkozzon újra később."; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "A kattintáshoz\nnyomja meg a gombot"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "A keződképernyő\nmegjelenítéséhez döntse meg"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Hawk Rattle\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Vatican Mask\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Chief's Mask\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "The Visionary\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "American Museum of Natural History\nExpedíciók"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "Frontiers of Flight múzeum"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "Endeavour űrsikló"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "Cueva del Indio\nPuerto Rico"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "Gunnuhver hőforrásai\nIzland"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "Spirit marsjáró\nA NASA/JPL képe"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(Kattintson a következőhöz induláshoz: %2$s)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "Párizs"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "Tokió"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "Velence"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "New York"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "Róma"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "Jeruzsálem"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "Monte-Carlo"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "London"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "Nagy-korallzátony"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "Túravezető"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "Saját videóim"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "KILÉPÉS"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "VISSZA A BEMUTATÓHOZ"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "A menühöz való visszatéréshez fordítsa el telefonját"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "A demókból való kilépéshez vegye ki telefonját a szemüvegből"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "KÖVETKEZŐ"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "A kiválasztáshoz nyomja meg a gombot."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "A visszalépéshez fordítsa el a szemüveget."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "Próbálja ki most."; diff --git a/paperscope/paperscope_launcher/resources/strings/id.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/id.lproj/cardboard.strings new file mode 100644 index 00000000..60915d28 --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/id.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "Tutorial"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "Video Selamat Datang"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "Penjelajah"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "Pameran"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "Jalan-jalan di Kota"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "Kaleidoskop"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "Perjalanan Arktik"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "Tidak dapat tersambung ke server\nperiksa sambungan internet Anda"; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "Terjadi kesalahan, coba lagi nanti"; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "Tekan tombol\nuntuk mengeklik"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "Goyangkan untuk menuju\nberanda"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Hawk Rattle\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Topeng Vatikan\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Topeng Kepala Suku\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "Visionary\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "Museum Sejarah Alam Amerika\nContoh Expedisi"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "Frontiers of Flight"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "Pesawat Ulang Alik Endeavour"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "Cueva del Indio\nPuerto Riko"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "Sumber Air Panas Gunnuhver\nIslandia"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "Mars Spirit Rover\nGambar dari NASA/JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(Klik untuk membuka %2$s)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "Paris"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "Tokyo"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "Venesia"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "New York"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "Roma"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "Yerusalem"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "Monte Carlo"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "London"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "Karang Penghalang Besar"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "Pemandu Tur"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "Video Saya"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "KELUAR"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "KEMBALI KE DEMO"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "Putar ponsel untuk kembali ke menu"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "Keluarkan ponsel dari penampil untuk keluar dari demo"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "BERIKUTNYA"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "Tekan tombol untuk memilih."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "Putar penampil Anda untuk bernavigasi kembali."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "Cobalah sekarang."; diff --git a/paperscope/paperscope_launcher/resources/strings/it.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/it.lproj/cardboard.strings new file mode 100644 index 00000000..f1809b78 --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/it.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "Tutorial"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "Video di benvenuto"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "Esploratore"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "Mostra"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "Passeggiata in città"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "Caleidoscopio"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "Viaggio in Artide"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "Impossibile connettersi al server.\nVerifica la connessione a Internet."; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "Si è verificato un errore. Riprova più tardi."; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "Premi il pulsante\nper fare clic"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "Inclina per andare alla\nhome"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Hawk Rattle\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Vatican Mask\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Chief's Mask\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "The Visionary\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "Museo americano di storia naturale\nEsempio di spedizione"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "Museo Frontiers of Flight"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "Navetta spaziale Endeavour"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "Cueva del Indio\nPortorico"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "Sorgenti termali di Gunnuhver\nIslanda"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "Veicolo spaziale Spirit su Marte\nImmagine di NASA/JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(Fai clic per andare a %2$s)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "Parigi"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "Tokyo"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "Venezia"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "New York"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "Roma"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "Gerusalemme"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "Monte Carlo"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "Londra"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "Grande Barriera Corallina"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "Guida turistica"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "I miei video"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "ESCI"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "TORNA ALLE DEMO"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "Ruota il telefono per tornare al menu"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "Togli lo smartphone dal visore per uscire dalle demo"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "AVANTI"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "Premi il pulsante per selezionare."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "Ruota il visore per tornare indietro."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "Prova adesso."; diff --git a/paperscope/paperscope_launcher/resources/strings/iw.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/iw.lproj/cardboard.strings new file mode 100644 index 00000000..97421100 --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/iw.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "מדריך"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "סרטון היכרות"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "סייר"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "הצגה"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "טיול רגלי בעיר"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "קליידוסקופ"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "מסע בקוטב הצפוני"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "לא ניתן להתחבר לשרת\nבדוק את החיבור לאינטרנט"; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "מצטערים, משהו השתבש. נסה שוב מאוחר יותר"; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "לחץ על הכפתור\nכדי להפעיל"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "הטה כדי לעבור\nלמסך הראשי"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Hawk Rattle\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Vatican Mask\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Chief's Mask\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "The Visionary\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "המוזיאון האמריקאי לתולדות הטבע\nדוגמה למסע"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "‏מוזיאון Frontiers of Flight"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "מעבורת החלל אנדוור"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "‏מערת Cueva del Indio\nפוארטו ריקו"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "‏מעיינות חמים ב-Gunnuhver\nאיסלנד"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "‏רכב המאדים 'ספיריט'\nתמונה מאת NASA/JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(לחץ כדי לעבור אל %2$s)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "פריס"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "טוקיו"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "ונציה"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "ניו יורק"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "רומא"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "ירושלים"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "מונטה קרלו"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "לונדון"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "שונית המחסום הגדולה"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "מדריך טיולים"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "הסרטונים שלי"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "יציאה"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "חזרה להדגמות"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "סובב את הטלפון כדי לחזור לתפריט"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "כדי לצאת מתצוגת ההדגמה צריך להוציא את הטלפון ממכשיר הצפייה"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "הבא"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "לחץ על הלחצן כדי לבחור."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "סובב את מכשיר הצפייה כדי לנווט חזרה."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "נסה זאת עכשיו."; diff --git a/paperscope/paperscope_launcher/resources/strings/ja.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/ja.lproj/cardboard.strings new file mode 100644 index 00000000..0675a8a9 --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/ja.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "チュートリアル"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "ウェルカム動画"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "探検"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "鑑賞"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "街を歩く"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "万華鏡"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "北極の旅"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "サーバーに接続できません\nインターネット接続をご確認ください"; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "エラーが発生しました。しばらくしてからもう一度お試しください"; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "ボタンを押して\nクリックします"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "傾けて\nホーム画面に戻ります"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Hawk Rattle\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Vatican Mask\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Chief's Mask\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "The Visionary\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "アメリカ自然史博物館\n探索のサンプル"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "最先端航空博物館"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "スペースシャトル・エンデバー"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "インディアンの洞窟\nプエルトリコ"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "グンヌクヴェル温泉\nアイスランド"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "マーズ・スピリット・ローバー\n画像提供: NASA/JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(クリックして%2$sに移動)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "パリ"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "東京"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "ベネチア"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "ニューヨーク"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "ローマ"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "エルサレム"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "モンテカルロ"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "ロンドン"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "グレートバリアリーフ"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "ツアーガイド"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "マイ動画"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "退出"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "デモに戻る"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "メニューに戻るには、スマートフォンを傾けます"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "デモを終了するには、スマートフォンをビューアから取り外します"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "次へ"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "選択するにはボタンを押します。"; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "ビューアを回転させると、前に戻ります。"; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "回転させてみてください。"; diff --git a/paperscope/paperscope_launcher/resources/strings/ko.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/ko.lproj/cardboard.strings new file mode 100644 index 00000000..39bb609f --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/ko.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "가이드"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "시작 동영상"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "탐험"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "전시"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "도시 하이킹"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "만화경"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "북극 여행"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "서버에 연결할 수 없습니다.\n인터넷 연결을 확인하세요."; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "오류가 발생했습니다. 나중에 다시 시도해 주세요."; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "클릭하려면\n버튼을 누르세요."; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "홈으로 이동하려면\n기울이세요."; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Hawk Rattle\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Vatican Mask\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Chief's Mask\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "The Visionary\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "미국 자연사 박물관\n탐험 샘플"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "비행기 박물관"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "엔데버 우주 왕복선"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "푸에르토리코\n꾸에바 델 인디오"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "아이슬란드\n군누베르 온천"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "화성 탐사선 스피릿\n이미지(출처: NASA/JPL)"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(클릭하여 %2$s(으)로 이동)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "파리"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "도쿄"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "베네치아"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "뉴욕"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "로마"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "예루살렘"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "몬테카를로"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "런던"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "그레이트 배리어 리프"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "어스"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "투어 가이드"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "내 동영상"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "종료"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "데모로 돌아가기"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "메뉴로 돌아가려면 스마트폰을 회전하세요."; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "데모를 종료하려면 스마트폰을 뷰어에서 꺼내세요."; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "다음"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "선택하려면 버튼을 누르세요."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "뒤로 이동하려면 뷰어를 회전하세요."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "지금 사용해 보세요."; diff --git a/paperscope/paperscope_launcher/resources/strings/lt.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/lt.lproj/cardboard.strings new file mode 100644 index 00000000..f0e6dff3 --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/lt.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "Mokymo programa"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "Pasveikinimo vaizdo įrašas"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "Tyrėjas"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "Ekspozicija"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "Žygis mieste"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "Kaleidoskopas"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "Arktinė kelionė"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "Nepavyksta prisijungti prie serverio.\nPatikrinkite interneto ryšį"; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "Apgailestaujame, kažkas ne taip. Vėliau bandykite dar kartą"; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "Paspauskite mygtuką,\nkad spustelėtumėte"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "Pakreipkite, kad patektumėte į\npagrindinį puslapį"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Hawk Rattle\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Vatican Mask\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Chief's Mask\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "The Visionary\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "Amerikos natūralios istorijos muziejus\nEkspedicijų pavyzdys"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "Frontiers of Flight"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "Endeavour Space Shuttle"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "Cueva del Indio\nPuerto Rikas"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "Gunnuhver Hot Springs\nIslandija"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "Mars Spirit Rover\nVaizdas iš NASA / JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(Spustelėkite, kad patektumėte į %2$s)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "Paryžius"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "Tokijas"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "Venecija"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "Niujorkas"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "Roma"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "Jeruzalė"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "Monte Karlas"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "Londonas"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "Didysis barjerinis rifas"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "Kelionės vadovas"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "Mano vaizdo įrašai"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "IŠEITI"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "ATGAL Į DEM. VERSIJĄ"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "Pasukite telefoną, kad grįžtumėte į meniu"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "Išimkite telefoną iš žiūriklio, kad išeitumėte iš demonstracinės versijos"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "KITAS"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "Paspauskite mygtuką, kad pasirinktumėte."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "Pasukite žiūriklį, kad grįžtumėte atgal."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "Išbandykite tai dabar."; diff --git a/paperscope/paperscope_launcher/resources/strings/lv.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/lv.lproj/cardboard.strings new file mode 100644 index 00000000..b2df864a --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/lv.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "Pamācība"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "Sveiciena videoklips"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "Atklājējs"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "Izstādes"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "Pārgājiens pilsētā"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "Kaleidoskops"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "Arctic Journey"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "Nevar izveidot savienojumu ar serveri.\nPārbaudiet interneta savienojumu."; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "Radās problēma. Lūdzu, vēlāk mēģiniet vēlreiz."; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "Nospiediet pogu,\nlai noklikšķinātu"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "Nolieciet, lai pārietu\nuz sākumu"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Maraka “Vanags”\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Maska “Vatikāns”\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Virsaiša maska\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "Sapņotājs\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Bēthovens\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "ASV Dabas vēstures muzejs\nEkspedīciju paraugs"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "Muzejs “Frontiers of Flight”"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "Kosmosa kuģis “Endeavour”"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "Indiāņu ala\nPuertoriko"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "Gunnuhver karstie avoti\nĪslande"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "Marsa izpētes robots “Spirit”\nNASA/JPL attēls"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(Noklikšķiniet, lai pārietu uz: %2$s)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "Parīze"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "Tokija"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "Venēcija"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "Ņujorka"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "Roma"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "Jeruzaleme"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "Montekarlo"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "Londona"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "Lielais Barjerrifs"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "Ceļvedis"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "Mani videoklipi"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "IZIET"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "UZ DEMONSTRĀCIJĀM"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "Lai atgrieztos izvēlnē, pagrieziet tālruni"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "Lai izietu no demonstrācijas režīma, noņemiet tālruni no skatītāja"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "TĀLĀK"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "Nospiediet pogu, lai veiktu atlasi."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "Pagrieziet skatītāju, lai pārietu atpakaļ."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "Izmēģiniet tūlīt!"; diff --git a/paperscope/paperscope_launcher/resources/strings/nb.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/nb.lproj/cardboard.strings new file mode 100644 index 00000000..775d778f --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/nb.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "Veiledning"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "Velkomstvideo"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "Utforsker"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "Utstilling"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "Gatevandring"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "Kaleidoskop"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "Reise i Arktis"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "Kan ikke koble til tjeneren\nsjekk internettforbindelsen din"; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "Beklager, noe gikk galt. Prøv igjen senere"; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "Trykk på knappen\nfor å klikke"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "Vipp for å gå\ntilbake til startskjermen"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Hawk Rattle\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Vatican Mask\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Chief's Mask\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "The Visionary\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "American Museum of Natural History\nsmakeprøve på Ekspedisjoner"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "Frontiers of Flight-museet"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "Endeavour-romfergen"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "Cueva del Indio-grotten\nPuerto Rico"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "Varme kilder i Gunnuhver\nIsland"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "Mars Spirit Rover\nBilde fra NASA/JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(Klikk for å gå til %2$s)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "Paris"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "Tokyo"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "Venezia"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "New York"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "Roma"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "Jerusalem"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "Monte Carlo"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "London"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "Great Barrier Reef"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "Omvisning med guide"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "Mine videoer"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "AVSLUTT"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "TILBAKE TIL DEMOENE"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "Rotér telefonen din for å gå tilbake til menyen"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "Ta telefonen ut av brillene for å gå ut av demoene"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "NESTE"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "Trykk på knappen for å velge."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "Rotér brillene for å navigere tilbake."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "Prøv det nå."; diff --git a/paperscope/paperscope_launcher/resources/strings/nl.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/nl.lproj/cardboard.strings new file mode 100644 index 00000000..1bbf6f07 --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/nl.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "Training"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "Welkomstvideo"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "Verkenner"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "Tentoonstelling"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "Stadswandeling"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "Kaleidoscoop"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "Poolreis"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "Kan geen verbinding maken met de server.\nControleer je internetverbinding."; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "Er is iets misgegaan. Probeer het later opnieuw."; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "Druk op de knop\nom te klikken"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "Kantel om naar\nde startpagina te gaan"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Hawk Rattle\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Vatican Mask\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Chief's Mask\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "The Visionary\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "American Museum of Natural History\nVoorbeeld van expedities"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "Frontiers of Flight"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "Spaceshuttle Endeavour"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "Cueva del Indio\nPuerto Rico"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "Thermale bronnen van Gunnuhver\nIJsland"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "Mars Spirit Rover\nAfbeelding van NASA/JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(Klik om naar %2$s te gaan)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "Parijs"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "Tokio"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "Venetië"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "New York"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "Rome"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "Jeruzalem"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "Monte Carlo"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "Londen"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "Great Barrier Reef"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "Tourgids"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "Mijn video's"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "SLUITEN"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "TERUG NAAR DEMO'S"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "Draai je telefoon om terug te gaan naar het menu"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "Haal je telefoon uit je bril om de demo's te verlaten"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "VOLGENDE"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "Druk op de knop om op een knop te klikken."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "Draai je bril om terug te navigeren."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "Probeer het nu."; diff --git a/paperscope/paperscope_launcher/resources/strings/pl.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/pl.lproj/cardboard.strings new file mode 100644 index 00000000..2dac651f --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/pl.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "Samouczek"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "Wideo powitalne"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "Odkrywca"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "Eksponat"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "Spacer po mieście"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "Kalejdoskop"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "Arktyczna podróż"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "Nie można połączyć się z serwerem.\nSprawdź swoje połączenie internetowe."; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "Coś poszło nie tak. Spróbuj ponownie później."; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "Naciśnij przycisk,\nby rozpocząć"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "Pochyl, aby przejść\nna stronę główną"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Hawk Rattle\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Vatican Mask\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Chief's Mask\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "The Visionary\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "Amerykańskie Muzeum Historii Naturalnej\nPrzykłady wypraw"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "Muzeum Frontiers of Flight"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "Prom kosmiczny Endeavour"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "Cueva del Indio\nPortoryko"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "Gorące źródła Gunnuhver\nIslandia"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "Bezzałogowa misja na Marsa Spirit\nZdjęcie NASA/JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(Kliknij, aby przejść do: %2$s)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "Paryż"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "Tokio"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "Wenecja"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "Nowy Jork"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "Rzym"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "Jerozolima"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "Monte Carlo"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "Londyn"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "Wielka Rafa Koralowa"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "Przewodnik po wycieczce"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "Moje filmy"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "ZAKOŃCZ"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "POWRÓT DO DEMO"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "Obróć telefon, aby wrócić do menu"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "Wyjmij telefon z gogli, aby zamknąć wersje demonstracyjne"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "DALEJ"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "Naciśnij przycisk, by wybrać."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "Obróć gogle, aby przejść wstecz."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "Wypróbuj teraz."; diff --git a/paperscope/paperscope_launcher/resources/strings/pt.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/pt.lproj/cardboard.strings new file mode 100644 index 00000000..1c6e2a4b --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/pt.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "Tutorial"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "Vídeo de boas-vindas"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "Explorador"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "Exposição"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "Caminhada urbana"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "Caleidoscópio"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "Jornada no Ártico"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "Não é possível se conectar ao servidor\nVerifique sua conexão com a Internet"; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "Ocorreu um erro. Tente novamente mais tarde"; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "Pressione o botão\npara clicar"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "Incline para acessar\na página inicial"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Máscara Hawk Rattle\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Máscara Vatican\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Máscara Chiefs\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "Máscara The Visionary\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Máscara Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "Museu Americano de História Natural\nExemplo de \"Expedições\""; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "Museu Frontiers of Flight"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "Ônibus espacial Endeavour"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "Caverna Cueva del Indio\nPorto Rico"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "Águas termais em Gunnuhver\nIslândia"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "Robô Spirit em Marte\nImagem de NASA/JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(Clique para ir até %2$s)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "Paris"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "Tóquio"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "Veneza"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "Nova York"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "Roma"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "Jerusalém"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "Monte Carlo"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "Londres"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "Grande Barreira de Corais"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Google Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "Guia de turismo"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "Meus vídeos"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "SAIR"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "VOLTAR DEMONSTRAÇÕES"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "Gire seu smartphone para voltar ao menu"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "Tire seu smartphone do visor para sair da demonstração"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "PRÓXIMA"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "Pressione o botão para selecionar."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "Gire seu visualizador para voltar na navegação."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "Tente fazer isso agora."; diff --git a/paperscope/paperscope_launcher/resources/strings/pt_BR.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/pt_BR.lproj/cardboard.strings new file mode 100644 index 00000000..1c6e2a4b --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/pt_BR.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "Tutorial"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "Vídeo de boas-vindas"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "Explorador"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "Exposição"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "Caminhada urbana"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "Caleidoscópio"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "Jornada no Ártico"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "Não é possível se conectar ao servidor\nVerifique sua conexão com a Internet"; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "Ocorreu um erro. Tente novamente mais tarde"; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "Pressione o botão\npara clicar"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "Incline para acessar\na página inicial"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Máscara Hawk Rattle\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Máscara Vatican\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Máscara Chiefs\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "Máscara The Visionary\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Máscara Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "Museu Americano de História Natural\nExemplo de \"Expedições\""; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "Museu Frontiers of Flight"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "Ônibus espacial Endeavour"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "Caverna Cueva del Indio\nPorto Rico"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "Águas termais em Gunnuhver\nIslândia"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "Robô Spirit em Marte\nImagem de NASA/JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(Clique para ir até %2$s)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "Paris"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "Tóquio"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "Veneza"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "Nova York"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "Roma"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "Jerusalém"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "Monte Carlo"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "Londres"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "Grande Barreira de Corais"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Google Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "Guia de turismo"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "Meus vídeos"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "SAIR"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "VOLTAR DEMONSTRAÇÕES"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "Gire seu smartphone para voltar ao menu"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "Tire seu smartphone do visor para sair da demonstração"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "PRÓXIMA"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "Pressione o botão para selecionar."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "Gire seu visualizador para voltar na navegação."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "Tente fazer isso agora."; diff --git a/paperscope/paperscope_launcher/resources/strings/pt_PT.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/pt_PT.lproj/cardboard.strings new file mode 100644 index 00000000..0498f383 --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/pt_PT.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "Tutorial"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "Vídeo de boas-vindas"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "Explorer"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "Exhibit"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "Urban Hike"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "Kaleidoscope"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "Viagem ao Ártico"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "Não é possível estabelecer a ligação ao servidor\nVerifique a ligação à Internet"; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "Passou-se qualquer coisa de errado. Tente novamente mais tarde."; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "Prima o botão\npara acionar"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "Inclinar para ir para\na Página inicial"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Hawk Rattle\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Vatican Mask\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Chief's Mask\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "The Visionary\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "American Museum of Natural History\nExemplos de expedições"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "Frontiers of Flight"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "Endeavour Space Shuttle"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "Cueva del Indio\nPorto Rico"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "Gunnuhver Hot Springs\nIslândia"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "Mars Spirit Rover\nImagem da NASA/JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(Clique para ir para %2$s)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "Paris"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "Tóquio"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "Veneza"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "Nova Iorque"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "Roma"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "Jerusalém"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "Monte Carlo"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "Londres"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "Grande Barreira de Coral"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "Tour Guide"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "My Videos"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "SAIR"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "VOLTAR ÀS DEMONSTR."; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "Rode o seu telemóvel para voltar ao menu"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "Retire o seu telemóvel do visualizador para sair das demonstrações"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "SEGUINTE"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "Prima o botão para selecionar."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "Rode o visualizador para navegar para trás."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "Experimente agora."; diff --git a/paperscope/paperscope_launcher/resources/strings/ro.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/ro.lproj/cardboard.strings new file mode 100644 index 00000000..ab6bb163 --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/ro.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "Tutorial"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "Videoclip de bun venit"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "Explorer"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "Exhibit"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "Urban Hike"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "Kaleidoscope"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "Călătorie polară"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "Conexiunea la server nu poate fi stabilită\nVerificați conexiunea la internet"; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "A apărut o eroare. Încercați din nou"; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "Apăsați pe buton\npână face clic"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "Înclinați pentru a accesa\necranul principal"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Masca Hawk Rattle\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Masca Vatican\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Masca Chief's\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "Masca Visionary\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Masca Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "Muzeul American de Istorie Naturală\nSecvență din Expediții"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "Muzeul Frontiers of Flight"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "Nava spațială Endeavour"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "Cueva del Indio\nPuerto Rico"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "Izvoarele termale de la Gunnuhver\nIslanda"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "Vehiculul spațial Spirit de pe Marte\nImagine de la NASA/JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(Dați clic pentru a accesa %2$s)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "Paris"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "Tokio"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "Veneția"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "New York"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "Roma"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "Ierusalim"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "Monte Carlo"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "Londra"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "Marea Barieră de Corali"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "Ghid turistic"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "Videoclipurile mele"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "IEȘI"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "ÎNAPOI LA DEMO"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "Rotiți telefonul pentru a reveni la meniu"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "Scoate telefonul din vizualizator pentru a ieși din versiunile demonstrative"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "ÎNAINTE"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "Apăsați pe buton pentru a selecta."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "Rotiți vizualizatorul pentru a naviga înapoi."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "Încercați acum."; diff --git a/paperscope/paperscope_launcher/resources/strings/ru.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/ru.lproj/cardboard.strings new file mode 100644 index 00000000..77392efa --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/ru.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "Руководство"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "Добро пожаловать!"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "Экскурсии"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "Выставка"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "Путешествия"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "Калейдоскоп"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "Арктические пейзажи"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "Не удается подключиться к серверу.\nПроверьте подключение к Интернету."; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "Произошла ошибка. Повторите попытку позже."; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "Нажмите на кнопку,\nчтобы выбрать"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "Наклоните, чтобы\nвернуться к началу"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Погремушка \"Ястреб\"\n© Ранде Кук"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Ватиканская маска\n© Ранде Кук"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Маска вождя\n© Ранде Кук"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "Прорицатель\n© Ранде Кук"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Бетховен\n© Ранде Кук"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "Американский музей естественной истории\nЭкспедиции"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "Музей Frontiers of Flight"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "Космический челнок \"Индевор\""; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "Пещера Куэва дель Индио\nПуэрто-Рико"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "Горячие источники Гуннухвер\nИсландия"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "Марсоход \"Спирит\"\nСнимок НАСА/ЛРД"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(нажмите, чтобы перейти далее: %2$s)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "Париж"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "Токио"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "Венеция"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "Нью-Йорк"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "Рим"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "Иерусалим"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "Монте-Карло"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "Лондон"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "Большой Барьерный риф"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Планета Земля"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "Экскурсия"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "Мои видео"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "ВЫЙТИ"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "НАЗАД К РОЛИКАМ"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "Чтобы вернуться в меню, поверните телефон"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "Чтобы закрыть демо, выньте телефон из очков"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "ДАЛЕЕ"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "Чтобы что-то выбрать, нажмите кнопку на очках."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "Поворачивайте очки, чтобы вернуться назад."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "Попробуйте повернуть сейчас."; diff --git a/paperscope/paperscope_launcher/resources/strings/sk.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/sk.lproj/cardboard.strings new file mode 100644 index 00000000..6db06be2 --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/sk.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "Príručka"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "Uvítacie video"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "Explorer"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "Exhibit"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "Urban Hike"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "Kaleidoscope"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "Arktická cesta"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "Nepodarilo sa pripojiť k serveru,\nskontrolujte svoje internetové pripojenie"; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "Ľutujeme, vyskytol sa problém, skúste to znova"; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "Spustite\nstlačením tlačidla"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "Naklonením prejdite\nna domovskú stránku"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Hawk Rattle\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Vatican Mask\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Chief's Mask\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "The Visionary\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "Múzeum American Museum of Natural History\nUkážka expedícií"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "Múzeum Frontiers of Flight"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "Raketopán Endeavour"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "Jaskyňa Cueva del Indio\nPortoriko"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "Termálne pramene Gunnuhver\nIsland"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "Vozidlo Spirit expedície na Mars\nObrázok NASA/JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(Kliknutím prejdite do umiestnenia %2$s)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "Paríž"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "Tokio"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "Benátky"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "New York"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "Rím"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "Jeruzalem"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "Monte Carlo"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "Londýn"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "Veľká koralová bariéra"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "Tour Guide"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "My Videos"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "UKONČIŤ"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "SPÄŤ NA UKÁŽKY"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "Otočením telefónu sa vráťte na ponuku"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "Ak chcete ukončiť demá, vyberte telefón zo zobrazovača"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "ĎALEJ"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "Výber uskutočníte stlačením tlačidla."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "Ak chcete prejsť späť, otočte zobrazovačom."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "Vyskúšajte to."; diff --git a/paperscope/paperscope_launcher/resources/strings/sl.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/sl.lproj/cardboard.strings new file mode 100644 index 00000000..bc9a4ca8 --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/sl.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "Vadnica"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "Pozdravni video"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "Raziskovalec"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "Razstava"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "Sprehod po mestu"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "Kalejdoskop"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "Popotovanje po Arktiki"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "S strežnikom se ni mogoče povezati\npreverite internetno povezavo"; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "Prišlo je do napake. Poskusite znova pozneje."; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "Pritisnite gumb,\nda kliknete"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "Nagnite za vrnitev\nna začetek"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Hawk Rattle\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Vatican Mask\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Chief's Mask\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "The Visionary\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "Ameriški prirodoslovni muzej\nPrimer za storitev Expeditions"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "Muzej Frontiers of Flight"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "Raketoplan Endeavour"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "Cueva del Indio\nPortoriko"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "Geotermalno območje Gunnuhver\nIslandija"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "Robot Spirit za raziskovanje Marsa\nSlika: NASA/JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(Kliknite za pomik na lokacijo %2$s)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "Pariz"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "Tokio"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "Benetke"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "New York"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "Rim"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "Jeruzalem"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "Monte Carlo"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "London"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "Veliki koralni greben"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "Turistični vodnik"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "Moji videoposnetki"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "IZHOD"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "PREDSTAVITVE"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "Če se želite vrniti v meni, zasukajte telefon"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "Za izhod iz predstavitvenih različic odstranite telefon iz pregledovalnika"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "NAPREJ"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "Za izbiro pritisnite gumb."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "Če se želite pomakniti nazaj, zasukajte glavo (s pregledovalnikom)."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "Poskusite zdaj."; diff --git a/paperscope/paperscope_launcher/resources/strings/sr.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/sr.lproj/cardboard.strings new file mode 100644 index 00000000..b79d192b --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/sr.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "Водич"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "Видео добродошлице"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "Истраживач"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "Изложба"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "Пешачење кроз град"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "Калеидоскоп"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "Арктичко путовање"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "Повезивање са сервером није успело\nПроверите интернет везу"; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "Жао нам је, нешто је пошло наопако. Покушајте поново касније."; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "Притисните дугме\nда бисте кликнули"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "Нагните да бисте отишли\nна почетак"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Звечка „Соко“\n© Ренд Кук"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Ватиканска маска\n© Ренд Кук"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Поглавичина маска\n© Ренд Кук"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "Визионар\n© Ренд Кук"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Бетовен\n© Ренд Кук"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "Амерички природњачки музеј\nПример експедиција"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "Музеј Frontiers of Flight"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "Спејс-шатл Ендевор"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "Cueva del Indio\nПорторико"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "Термални извори Гунухвер\nИсланд"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "Ровер Спирит на Марсу\nСлика: НАСА/Лабораторија за млазни погон"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(Кликните да бисте прешли на %2$s)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "Париз"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "Токио"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "Венеција"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "Њујорк"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "Рим"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "Јерусалим"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "Монте Карло"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "Лондон"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "Велики корални гребен"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "Туристички водич"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "Моји видео снимци"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "ЗАТВОРИ"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "НАЗАД"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "Окрените телефон да бисте се вратили у мени"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "Извадите телефон из маске да бисте изашли из демо верзија"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "ДАЉЕ"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "Притисните дугме да бисте изабрали."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "Ротирајте маску да бисте се вратили."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "Испробајте одмах."; diff --git a/paperscope/paperscope_launcher/resources/strings/sv.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/sv.lproj/cardboard.strings new file mode 100644 index 00000000..06d45c90 --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/sv.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "Självstudier"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "Välkomstvideo"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "Explorer"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "Exhibit"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "Urban Hike"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "Kaleidoscope"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "Res till Nordpolen"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "Det går inte att ansluta till servern\nKontrollera internetanslutningen"; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "Något gick fel, försök igen senare"; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "Tryck på knappen\nför att klicka"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "Luta för att återgå till\nstart"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Hawk Rattle\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Vatican Mask\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Chief's Mask\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "The Visionary\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "American Museum of Natural History\nExpeditions-exempel"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "Frontiers of Flight"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "Rymdfärjan Endeavour"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "Cueva del Indio\nPuerto Rico"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "De varma källorna i Gunnuhver\nIsland"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "Rovern Spirit på Mars\nBild från NASA/JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(Klicka om du vill gå till %2$s)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "Paris"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "Tokyo"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "Venedig"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "New York"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "Rom"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "Jerusalem"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "Monte Carlo"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "London"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "Stora Barriärrevet"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "Reseledaren"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "Mina videoklipp"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "AVSLUTA"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "TILLBAKA TILL DEMOS"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "Om du roterar telefonen visas menyn igen"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "Ta ut telefonen ur kikaren om du vill stänga demorna"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "NÄSTA"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "Välj genom att trycka på knappen."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "Rotera kikaren om du vill navigera bakåt."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "Testa nu."; diff --git a/paperscope/paperscope_launcher/resources/strings/th.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/th.lproj/cardboard.strings new file mode 100644 index 00000000..896d5e6a --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/th.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "บทแนะนำ"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "วิดีโอต้อนรับ"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "นักสำรวจ"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "นิทรรศการ"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "เดินในเมือง"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "ภาพลานตา"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "เส้นทางอาร์กติก"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ได้\nโปรดตรวจดูการเชื่อมต่ออินเทอร์เน็ต"; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "ขออภัย เกิดข้อผิดพลาดขึ้น โปรดลองอีกครั้งในภายหลัง"; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "กดปุ่ม\nเพื่อคลิก"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "เอียงเพื่อไปยัง\nหน้าแรก"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Hawk Rattle\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Vatican Mask\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Chief's Mask\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "The Visionary\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "พิพิธภัณฑ์ประวัติศาสตร์ธรรมชาติอเมริกัน\nตัวอย่างการออกสำรวจ"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "พิพิธภัณฑ์ Frontiers of Flight"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "กระสวยอวกาศเอนเดฟเวอร์"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "ถ้ำ Cueva del Indio\nประเทศเปอร์โตริโก"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "น้ำพุร้อน Gunnuhver\nประเทศไอซ์แลนด์"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "ยานสำรวจดาวอังคาร Spirit\nภาพจาก NASA/JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(คลิกเพื่อไปที่ %2$s)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "ปารีส"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "โตเกียว"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "เวนิส"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "นิวยอร์ก"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "โรม"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "เยรูซาเลม"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "มอนติคาร์โล"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "ลอนดอน"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "เกรตแบร์ริเออร์รีฟ"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "มัคคุเทศก์"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "วิดีโอของฉัน"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "ออก"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "กลับไปที่การสาธิต"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "หมุนโทรศัพท์เพื่อกลับไปที่เมนู"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "แกะโทรศัพท์ออกจากแว่นเพื่อออกจากการสาธิต"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "ถัดไป"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "กดปุ่มเพื่อเลือก"; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "หมุนกล่องเพื่อย้อนกลับไป"; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "ลองใช้ดู"; diff --git a/paperscope/paperscope_launcher/resources/strings/tr.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/tr.lproj/cardboard.strings new file mode 100644 index 00000000..97003408 --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/tr.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "Eğitici"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "Karşılama Videosu"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "Gezgin"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "Sergi"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "Şehirde Yürüyüş"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "Kaleydoskop"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "Kuzey Kutbu'na Yolculuk"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "Sunucuya bağlanılamıyor,\nİnternet bağlantınızı kontrol edin"; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "Bir sorun oluştu, lütfen daha sonra tekrar deneyin"; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "Tıklamak için\ndüğmeye basın"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "Başa dönmek için\neğin"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Hawk Rattle\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Vatican Mask\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Chief's Mask\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "The Visionary\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "Amerikan Doğa Tarihi Müzesi\nSergi Örneği"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "Frontiers of Flight"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "Endeavour Uzay Mekiği"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "Cueva del Indio\nPorto Riko"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "Gunnuhver Termal Kaplıcaları\nİzlanda"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "Mars Keşif Aracı Spirit\nNASA/JPL'den gelen resim"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(%2$s konumuna gitmek için tıklayın)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "Paris"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "Tokyo"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "Venedik"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "New York"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "Roma"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "Kudüs"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "Monte Carlo"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "Londra"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "Büyük Set Resifi"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "Tour Guide"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "My Videos"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "ÇIK"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "DEMOLARA DÖN"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "Menüye dönmek için telefonunuzu döndürün"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "Demolardan çıkmak için telefonunuzu gözlükten çıkarın"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "SONRAKİ"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "Seçmek için düğmeye basın."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "Geri gitmek için gözlüğünüzü döndürün."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "Şimdi deneyin."; diff --git a/paperscope/paperscope_launcher/resources/strings/uk.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/uk.lproj/cardboard.strings new file mode 100644 index 00000000..292e29c4 --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/uk.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "Навчальний посібник"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "Привітальне відео"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "Дослідник"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "Виставка"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "Прогулянка містом"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "Калейдоскоп"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "Арктична подорож"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "Не вдається з’єднатися із сервером\nперевірте з’єднання з Інтернетом"; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "Сталася помилка, повторіть спробу пізніше"; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "Натисніть кнопку\n"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "Нахиліть окуляри,\nщоб повернутися на початок"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Брязкальце-яструб\n© Ренді Кук"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Ватиканська маска\n© Ренді Кук"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Маска вождя\n© Ренді Кук"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "Провидець\n© Ренді Кук"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Бетховен\n© Ренді Кук"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "Американський музей природознавства\nзразок експедиції"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "Музей Frontiers of Flight"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "Космічний корабель \"Індевор\""; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "Куева дель Індіо\nПуерто-Рико"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "Термальні джерела Гуннухвер\nІсландія"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "Марсохід \"Спіріт\"\nЗображення NASA/JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(Натисніть, щоб перейти до об’єкта \"%2$s\")"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "Париж"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "Токіо"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "Венеція"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "Нью-Йорк"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "Рим"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "Єрусалим"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "Монте-Карло"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "Лондон"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "Великий бар’єрний риф"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Планета Земля"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "Путівник"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "Мої відео"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "ВИЙТИ"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "ДО ДЕМО-РОЛИКІВ"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "Оберніть телефон, щоб повернутися до меню"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "Вийміть телефон з окулярів, щоб вийти з режиму демонстрації"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "ДАЛІ"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "Натисніть кнопку, щоб вибрати."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "Оберніть окуляри, щоб повернутися назад."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "Спробуйте зараз!"; diff --git a/paperscope/paperscope_launcher/resources/strings/vi.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/vi.lproj/cardboard.strings new file mode 100644 index 00000000..5f35aacd --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/vi.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "Hướng dẫn"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "Video chào mừng"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "Khám phá"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "Triển lãm"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "Đi dạo trên phố"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "Kính vạn hoa"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "Chuyến phiêu lưu tới Bắc Cực"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "Không thể kết nối với máy chủ\nhãy kiểm tra kết nối internet của bạn"; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "Rất tiếc vì đã xảy ra sự cố, vui lòng thử lại sau"; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "Nhấn nút\nđể nhấp vào"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "Nghiêng để về\ntrình chạy chính"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Mặt nạ diều hâu\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Mặt nạ Vatican\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Mặt nạ thủ lĩnh\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "Mặt nạ ảo mộng\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Mặt nạ Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "Bảo tàng lịch sử tự nhiên Hoa Kỳ\nVí dụ cho Expeditions"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "Bảo tàng Frontiers of Flight"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "Tàu con thoi Endeavour"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "Cueva del Indio\nPuerto Rico"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "Suối nước nóng Gunnuhver\nIceland"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "Rô bốt tự hành thám hiểm sao Hỏa Spirit \nHình ảnh từ NASA/JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(Nhấp vào để chuyển tới %2$s)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "Paris"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "Tokyo"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "Venice"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "New York"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "Rome"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "Jerusalem"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "Monte Carlo"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "Luân Đôn"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "Great Barrier Reef"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Earth"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "Hướng dẫn chuyến đi"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "Video của tôi"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "THOÁT"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "VỀ LẠI B.TRÌNH DIỄN"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "Xoay điện thoại của bạn để quay lại menu"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "Lấy điện thoại ra khỏi thiết bị xem để thoát khỏi bản minh hoạ"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "TIẾP THEO"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "Nhấn nút để chọn."; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "Xoay thiết bị xem của bạn để điều hướng trở lại."; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "Hãy làm thử ngay bây giờ."; diff --git a/paperscope/paperscope_launcher/resources/strings/zh_CN.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/zh_CN.lproj/cardboard.strings new file mode 100644 index 00000000..e37855f6 --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/zh_CN.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "教程"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "欢迎视频"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "探险者"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "展览"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "城市远足"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "万花筒"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "北极之旅"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "无法连接到服务器\n请检查您的互联网连接"; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "抱歉,出了点问题。请重试"; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "按下按钮\n即可点击"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "倾斜 Cardboard 以转到\n首页"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Hawk Rattle\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Vatican Mask\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Chief's Mask\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "The Visionary\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "美国自然历史博物馆\n实境教学样本"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "飞行先驱者"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "奋进号航天飞机"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "印第安洞穴\n波多黎各"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "格林达维克温泉\n冰岛"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "勇气号火星探测器\n图片来源:NASA/JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(点击即可转到%2$s)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "巴黎"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "东京"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "威尼斯"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "纽约"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "罗马"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "耶路撒冷"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "蒙特卡洛"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "伦敦"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "大堡礁"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "Google 地球"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "导览"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "我的视频"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "退出"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "返回演示影片"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "旋转手机即可返回菜单"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "将手机从观看器中取出即可退出演示"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "下一步"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "按此按钮即可选择。"; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "旋转眼镜即可返回。"; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "立即尝试。"; diff --git a/paperscope/paperscope_launcher/resources/strings/zh_HK.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/zh_HK.lproj/cardboard.strings new file mode 100644 index 00000000..11b9e904 --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/zh_HK.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "教學課程"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "歡迎使用影片"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "探險家"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "展覽館"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "都市行腳"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "萬花筒"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "北極之旅"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "無法連線到伺服器\n請檢查您的網際網路連線"; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "發生錯誤,請稍後再試"; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "按下按鈕\n"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "將 Cardboard 傾斜即可\n回到啟動器主畫面"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Hawk Rattle\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Vatican Mask\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Chief's Mask\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "The Visionary\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "美國自然史博物館\n探險考察樣品"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "飛行里程碑博物館"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "奮進號太空梭"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "印第安岩洞\n波多黎各"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "格奴弗爾溫泉\n冰島"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "精神號火星探測車\n圖片來源:NASA/JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(按一下即可前往 %2$s)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "巴黎"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "東京"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "威尼斯"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "紐約"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "羅馬"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "耶路撒冷"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "蒙地卡羅"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "倫敦"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "大堡礁"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "地球"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "導覽"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "我的影片"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere 全景相片"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "離開"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "返回示範影片"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "旋轉您的手機以返回選單"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "將手機從觀影盒中取出,即可退出示範模式"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "下一步"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "按下按鈕以選取。"; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "旋轉您的檢視器即可返回。"; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "立即體驗。"; diff --git a/paperscope/paperscope_launcher/resources/strings/zh_TW.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/zh_TW.lproj/cardboard.strings new file mode 100644 index 00000000..9a523760 --- /dev/null +++ b/paperscope/paperscope_launcher/resources/strings/zh_TW.lproj/cardboard.strings @@ -0,0 +1,197 @@ +/* + * cardboard.strings + * + * Guidelines: + * + * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey. + * + * 2/ For descriptions, use 100-column limit and align multiple lines on the first character. + * For strings, do not break lines. + * + * 3/ Write a meaningful description, so the translator can know what exactly you are talking about. + * + * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a + * noun or a verb and they have different translations in other languages. Add the grammar + * class, the verb tense or any other information to distinguish between homonyms. + * + * 5/ Supply the maximum number of characters for strings in the comment. For example: + * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/ + * "WarpModeButtonLabel" = "Warp Speed"; + * + * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally + * accessibility strings do not have a limit, because they are spoken. Indicate this with (no + * limit). + */ + +/* Title of tutorial demo. (30 chars) */ +"TutorialDemoTitle" = "教學課程"; + +/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */ +"WelcomeVideoTitle" = "歡迎使用影片"; + +/* Title of explorer demo. (30 chars) */ +"ExplorerDemoTitle" = "探險家"; + +/* Title of exhibit demo. (30 chars) */ +"ExhibitDemoTitle" = "展覽館"; + +/* Title of urban hike demo. (30 chars) */ +"UrbanHikeDemoTitle" = "都市行腳"; + +/* Title of exhibit demo. (30 chars) */ +"KaleidoscopeDemoTitle" = "萬花筒"; + +/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */ +"ArcticJourneyDemoTitle" = "北極之旅"; + +/* Message shown to the user if there is a problem connecting to the server. (80 chars) */ +"ConnectionErrorMessage" = "無法連線到伺服器\n請檢查您的網際網路連線"; + +/* Message shown to the user if there was some generic error. (80 chars) */ +"GenericErrorMessage" = "發生錯誤,請稍後再試"; + +/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */ +"TutorialClickCaption" = "按下按鈕\n"; + +/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher. + (60 chars) */ +"TutorialTiltCaption" = "將 Cardboard 傾斜即可\n回到啟動器主畫面"; + +/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */ +"HawkRattle" = "Hawk Rattle\n© Rande Cook"; + +/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */ +"VaticanMask" = "Vatican Mask\n© Rande Cook"; + +/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */ +"ChiefsMask" = "Chief's Mask\n© Rande Cook"; + +/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */ +"TheVisionary" = "The Visionary\n© Rande Cook"; + +/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */ +"Beethoven" = "Beethoven\n© Rande Cook"; + +/* + * Title shown for sample panorama inside the American Museum of Natural History. + * (No limit) + */ +"AmericanMuseumNaturalHistory" = "美國自然史博物館\n探險考察樣品"; + +/* + * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX. + * (No limit) + */ +"FrontiersOfFlight" = "飛行里程碑博物館"; + +/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */ +"EndeavourSpaceShuttle" = "奮進號太空梭"; + +/* + * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico. + * (No limit) + */ +"CuevaDelIndio" = "印第安岩洞\n波多黎各"; + +/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */ +"GunnuhverHotSprings" = "格奴弗爾溫泉\n冰島"; + +/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */ +"MarsSpiritRover" = "精神號火星探測車\n圖片來源:NASA/JPL"; + +/* + * Label that reports the user's current location with an addtional prompt to go to the next + * location. The first parameter is the current location, the second parameter is the next + * location. (No limit) + */ +"UrbanHikeCurrentLocation" = "%1$s\n(按一下即可前往 %2$s)"; + +/* Location name referencing Paris France in the Urban Hike demo. (No limit) */ +"UrbanHikeParis" = "巴黎"; + +/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */ +"UrbanHikeTokyo" = "東京"; + +/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeVenice" = "威尼斯"; + +/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */ +"UrbanHikeNewYork" = "紐約"; + +/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */ +"UrbanHikeRome" = "羅馬"; + +/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */ +"UrbanHikeJerusalem" = "耶路撒冷"; + +/* + * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo, + * in the Urban Hike demo. (No limit) +*/ +"UrbanHikeMonteCarlo" = "蒙地卡羅"; + +/* Location name referencing London England in the Urban Hike demo. (No limit) */ +"UrbanHikeLondon" = "倫敦"; + +/* + * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia) + * in the Urban Hike demo. (No limit) + */ +"UrbanHikeGreatBarrierReef" = "大堡礁"; + +/* + * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars) + * 'Earth' should be the translated the same way as the official 'Google Earth' product. + */ +"EarthAndroidDemoTitle" = "地球"; + + * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour + * guide around Versailles. (30 chars) + */ +"TourGuideAndroidDemoTitle" = "導覽"; + +/* + * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all + * of their local videos in Cardboard as if viewing them on a large screen in front of them. + * (30 chars) + */ +"MyVideosAndroidDemoTitle" = "我的影片"; + +/* + * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and + * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical + * Google Photo Sphere product name. (30 chars) + */ +"PhotoSphereAndroidDemoTitle" = "Photo Sphere 全景相片"; + +/* + * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story + * where a hat is blown around by the wind. It should be considered a product name, and should + * be kept in English for languages that normally keep Google product names in English. (30 chars) + */ +"WindyDayAndroidDemoTitle" = "Windy Day"; + +/* Title of button exit VR. (20 chars) */ +"Exit" = "結束"; + +/* Title of button to go back to the Cardboard demos. (20 chars) */ +"BackToDemos" = "返回示範影片"; + +/* Message telling the user that they can tilt their phone to go back. + (no limit) */ +"RotateToGoBack" = "旋轉您的手機以返回選單"; + +/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */ +"TakeYourPhoneOutToExitDemos" = "將手機從觀影盒中取出,即可退出示範模式"; + +* Title of button for user to click to go to next step. (10 chars) */ +"Next" = "下一步"; + +/* Message telling the user to press the button on the viewer to click a button. (no limit) */ +"PressTheButtonOnYourViewer" = "按下按鈕以選取。"; + +/* Message telling the user to rotate the viewer to navigate back. (no limit) */ +"RotateYourViewer" = "旋轉您的檢視器即可返回。"; + +/* Message telling the user to try rotating the view now. (no limit) */ +"TryItOutNow" = "立即體驗。"; diff --git a/paperscope/paperscope_launcher/src/animated_shape.cc b/paperscope/paperscope_launcher/src/animated_shape.cc new file mode 100644 index 00000000..30b26418 --- /dev/null +++ b/paperscope/paperscope_launcher/src/animated_shape.cc @@ -0,0 +1,81 @@ +/* + * Copyright 2024 Google LLC + * + * 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. + */ +#include "animated_shape.h" + +#include + +#include // NOLINT(build/c++11) +#include "util.h" + +namespace cardboard_launcher { + +// Duration of each frame in the animation. +const std::chrono::milliseconds kAnimationFrameDuration(67); + +void AnimatedShape::SetModelViewProjection( + const cardboard_demo::Matrix4x4& eye_view, + const cardboard_demo::Matrix4x4& projection_matrix) { + cardboard_demo::Matrix4x4 model_view_tutorial = eye_view * model_animation_; + model_view_projection_animation_ = projection_matrix * model_view_tutorial; +} + +void AnimatedShape::LoadAnimations(GLuint obj_position_param_, + GLuint obj_uv_param_, + AAssetManager* asset_mgr_, JNIEnv* env, + jobject java_asset_mgr) { + env_ = env; + java_asset_mgr_ = java_asset_mgr; + for (int i = 0; i < static_cast(animation_frames_.size()); ++i) { + CARDBOARDDEMO_CHECK(animation_meshes_[i].Initialize( + obj_position_param_, obj_uv_param_, "IconMesh.obj", asset_mgr_)); + CARDBOARDDEMO_CHECK(animation_textures_[i].Initialize( + env, java_asset_mgr_, animation_frames_[i])); + } + + // Z- points forward, set the animation to be in front of the user, + // facing the user. + float yaw = 180.0f * M_PI / 180.0f; // rotate 180 degrees. + model_animation_ = cardboard_demo::GetTranslationMatrix({0, 2.2f, -1.5}) * + cardboard_demo::GetYawRotationMatrix(yaw); + cardboard_demo::CHECKGLERROR("LoadAnimations"); +} + +void AnimatedShape::Init() { + for (auto& texture : animation_textures_) { + texture.set_texture_id(0); + } +} + +void AnimatedShape::DisplayFrame( + const std::chrono::steady_clock::time_point timestamp) { + if (!animation_textures_[animation_index_].get_texture_id() || + !glIsTexture(animation_textures_[animation_index_].get_texture_id())) { + CARDBOARDDEMO_CHECK(animation_textures_[animation_index_].Initialize( + env_, java_asset_mgr_, animation_frames_[animation_index_])); + } + animation_textures_[animation_index_].Bind(); + animation_meshes_[animation_index_].Draw(); + + if (!animation_frames_.empty()) { + if (timestamp - last_update_time_ >= kAnimationFrameDuration) { + animation_index_ = (animation_index_ + 1) % animation_frames_.size(); + last_update_time_ = std::chrono::steady_clock::now(); + } + } + cardboard_demo::CHECKGLERROR("DisplayFrame"); +} + +} // namespace cardboard_launcher diff --git a/paperscope/paperscope_launcher/src/animated_shape.h b/paperscope/paperscope_launcher/src/animated_shape.h new file mode 100644 index 00000000..a8b05ff4 --- /dev/null +++ b/paperscope/paperscope_launcher/src/animated_shape.h @@ -0,0 +1,120 @@ +/* + * Copyright 2024 Google LLC + * + * 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. + */ +#ifndef PAPERSCOPE_LAUNCHER_SRC_ANIMATED_SHAPE_H_ +#define PAPERSCOPE_LAUNCHER_SRC_ANIMATED_SHAPE_H_ + +#include +#include + +#include // NOLINT(build/c++11) +#include + +#include +#include "util.h" + +namespace cardboard_launcher { + +// An animated shape is a renderable that renders a series of images in order +// to create frame-by-frame animations. +class AnimatedShape { + public: + AnimatedShape() = default; + + /** + * Creates an AnimatedShape from a vector of animation frames. + * + * @param animation_frames A vector of animation frames. + */ + AnimatedShape(const std::vector& animation_frames) + : animation_frames_(animation_frames), + last_update_time_(std::chrono::steady_clock::now()), + animation_index_(0), + animation_meshes_(animation_frames.size()), + animation_textures_(animation_frames.size()) {} + + /** + * Gets the model matrix for the animation. + * + * @return The model matrix for the animation. + */ + cardboard_demo::Matrix4x4 GetModelAnimation() const { + return model_animation_; + } + + /** + * Gets the modelview projection matrix for the animation. + * + * @return The modelview projection matrix for the animation. + */ + cardboard_demo::Matrix4x4 GetModelViewProjectionAnimation() const { + return model_view_projection_animation_; + } + /** + * Sets the modelview projection matrix for the animation. + * + * @param eye_view The eye view matrix. + * @param projection_matrix The projection matrix. + */ + void SetModelViewProjection( + const cardboard_demo::Matrix4x4& eye_view, + const cardboard_demo::Matrix4x4& projection_matrix); + + /** + * Loads the animations from the given asset manager. + * + * @param obj_position_param The object position parameter. + * @param obj_uv_param The object uv parameter. + * @param asset_mgr The asset manager. + * @param env The JNI environment. + * @param java_asset_mgr The Java asset manager. + */ + void LoadAnimations(GLuint obj_position_param_, GLuint obj_uv_param_, + AAssetManager* asset_mgr_, JNIEnv* env, + jobject java_asset_mgr_); + + /** + * Prepares the animation frame for the current timestamp and updates the + * animation index if necessary. + * + * @param timestamp The current timestamp. + */ + void DisplayFrame(std::chrono::steady_clock::time_point timestamp); + + // Initializes the animated shape. + void Init(); + + private: + // List of animation frames. + std::vector animation_frames_; + // Last time the animation was updated. + std::chrono::steady_clock::time_point last_update_time_; + // Index of the current animation. + int animation_index_; + + // Model and modelview projection matrices for the animation. + cardboard_demo::Matrix4x4 model_animation_; + cardboard_demo::Matrix4x4 model_view_projection_animation_; + std::vector animation_meshes_; + std::vector animation_textures_; + + // JNI environment and Java asset manager for reloading textures. + JNIEnv* env_ = nullptr; + jobject java_asset_mgr_ = nullptr; +}; + +} // namespace cardboard_launcher + +#endif // PAPERSCOPE_LAUNCHER_SRC_ANIMATED_SHAPE_H_ diff --git a/paperscope/paperscope_launcher/src/cardboard_jni.cc b/paperscope/paperscope_launcher/src/cardboard_jni.cc new file mode 100644 index 00000000..c723fb22 --- /dev/null +++ b/paperscope/paperscope_launcher/src/cardboard_jni.cc @@ -0,0 +1,222 @@ +/* + * Copyright 2024 Google LLC + * + * 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. + */ +#include +#include + +#include +#include + +#include "cardboard_launcher.h" +#include "magic_window.h" +#include "main.h" +#include "settings.h" + +#define JNI_LAUNCHER_METHOD(return_type, method_name) \ + JNIEXPORT return_type JNICALL \ + Java_com_google_vr_cardboard_paperscope_demo_CardboardLauncherActivity_##method_name // NOLINT + +#define JNI_MAGIC_WINDOW_METHOD(return_type, method_name) \ + JNIEXPORT return_type JNICALL \ + Java_com_google_vr_cardboard_paperscope_demo_magicwindow_MagicWindowActivity_##method_name // NOLINT + +#define JNI_SETTINGS_METHOD(return_type, method_name) \ + JNIEXPORT return_type JNICALL \ + Java_com_google_vr_cardboard_paperscope_demo_settings_SettingsActivity_##method_name // NOLINT + +#define JNI_MAIN_METHOD(return_type, method_name) \ + JNIEXPORT return_type JNICALL \ + Java_com_google_vr_cardboard_paperscope_demo_MainActivity_##method_name // NOLINT + +namespace { + +inline jlong launcher_jptr(cardboard_launcher::CardboardLauncher* native_app) { + return reinterpret_cast(native_app); +} + +inline cardboard_launcher::CardboardLauncher* launcher_native(jlong ptr) { + return reinterpret_cast(ptr); +} + +inline jlong magic_window_jptr(magic_window::MagicWindow* native_app) { + return reinterpret_cast(native_app); +} + +inline magic_window::MagicWindow* magic_window_native(jlong ptr) { + return reinterpret_cast(ptr); +} + +inline jlong settings_jptr(settings::Settings* native_app) { + return reinterpret_cast(native_app); +} + +inline settings::Settings* settings_native(jlong ptr) { + return reinterpret_cast(ptr); +} + +inline jlong main_jptr(main::Main* native_app) { + return reinterpret_cast(native_app); +} + +inline main::Main* main_native(jlong ptr) { + return reinterpret_cast(ptr); +} + +JavaVM* javaVm; + +} // anonymous namespace + +extern "C" { + +JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* /*reserved*/) { + javaVm = vm; + return JNI_VERSION_1_6; +} + +// Cardboard Launcher Functions +JNI_LAUNCHER_METHOD(jlong, nativeOnCreate) +(JNIEnv* /*env*/, jobject obj, jobject asset_mgr) { + return launcher_jptr( + new cardboard_launcher::CardboardLauncher(javaVm, obj, asset_mgr)); +} + +JNI_LAUNCHER_METHOD(void, nativeOnDestroy) +(JNIEnv* /*env*/, jobject /*obj*/, jlong native_app) { + delete launcher_native(native_app); +} + +JNI_LAUNCHER_METHOD(void, nativeOnSurfaceCreated) +(JNIEnv* env, jobject /*obj*/, jlong native_app) { + launcher_native(native_app)->OnSurfaceCreated(env); +} + +JNI_LAUNCHER_METHOD(void, nativeOnDrawFrame) +(JNIEnv* /*env*/, jobject /*obj*/, jlong native_app) { + launcher_native(native_app)->OnDrawFrame(); +} + +JNI_LAUNCHER_METHOD(void, nativeOnTriggerEvent) +(JNIEnv* /*env*/, jobject /*obj*/, jlong native_app) { + launcher_native(native_app)->OnTriggerEvent(); +} + +JNI_LAUNCHER_METHOD(void, nativeOnPause) +(JNIEnv* /*env*/, jobject /*obj*/, jlong native_app) { + launcher_native(native_app)->OnPause(); +} + +JNI_LAUNCHER_METHOD(void, nativeOnResume) +(JNIEnv* /*env*/, jobject /*obj*/, jlong native_app) { + launcher_native(native_app)->OnResume(); +} + +JNI_LAUNCHER_METHOD(void, nativeSetScreenParams) +(JNIEnv* /*env*/, jobject /*obj*/, jlong native_app, jint width, jint height) { + launcher_native(native_app)->SetScreenParams(width, height); +} + +JNI_LAUNCHER_METHOD(void, nativeSwitchViewer) +(JNIEnv* /*env*/, jobject /*obj*/, jlong native_app) { + launcher_native(native_app)->SwitchViewer(); +} + +JNI_LAUNCHER_METHOD(bool, nativeShouldLaunchDemo) +(JNIEnv* /*env*/, jobject /*obj*/, jlong native_app) { + return launcher_native(native_app)->ShouldLaunchDemo(); +} + +JNI_LAUNCHER_METHOD(jstring, nativeDemoToLaunch) +(JNIEnv* env, jobject /*obj*/, jlong native_app) { + return env->NewStringUTF( + (launcher_native(native_app)->DemoToLaunch()).c_str()); +} + +JNI_LAUNCHER_METHOD(void, nativeDemoLaunched) +(JNIEnv* /*env*/, jobject /*obj*/, jlong native_app) { + launcher_native(native_app)->DemoLaunched(); +} + +// Cardboard Magic Window Functions +JNI_MAGIC_WINDOW_METHOD(jlong, nativeOnCreate) +(JNIEnv* /*env*/, jobject obj, jobject asset_mgr) { + return magic_window_jptr( + new magic_window::MagicWindow(javaVm, obj, asset_mgr)); +} + +JNI_MAGIC_WINDOW_METHOD(void, nativeOnDestroy) +(JNIEnv* /*env*/, jobject /*obj*/, jlong native_app) { + delete magic_window_native(native_app); +} + +JNI_MAGIC_WINDOW_METHOD(void, nativeOnSurfaceCreated) +(JNIEnv* env, jobject /*obj*/, jlong native_app) { + magic_window_native(native_app)->OnSurfaceCreated(env); +} + +JNI_MAGIC_WINDOW_METHOD(void, nativeOnDrawFrame) +(JNIEnv* /*env*/, jobject /*obj*/, jlong native_app) { + magic_window_native(native_app)->OnDrawFrame(); +} + +JNI_MAGIC_WINDOW_METHOD(void, nativeOnResume) +(JNIEnv* /*env*/, jobject /*obj*/, jlong native_app) { + magic_window_native(native_app)->OnResume(); +} + +JNI_MAGIC_WINDOW_METHOD(void, nativeOnPause) +(JNIEnv* /*env*/, jobject /*obj*/, jlong native_app) { + magic_window_native(native_app)->OnPause(); +} + +JNI_MAGIC_WINDOW_METHOD(void, nativeSetScreenParams) +(JNIEnv* /*env*/, jobject /*obj*/, jlong native_app, jint width, jint height) { + magic_window_native(native_app)->SetScreenParams(width, height); +} + +JNI_MAGIC_WINDOW_METHOD(void, nativeScanViewer) +(JNIEnv* /*env*/, jobject /*obj*/, jlong native_app) { + magic_window_native(native_app)->ScanViewer(); +} + +JNI_MAGIC_WINDOW_METHOD(bool, nativeHasSavedDeviceParams) +(JNIEnv* /*env*/, jobject /*obj*/, jlong native_app) { + return magic_window_native(native_app)->HasSavedDeviceParams(); +} + +// Cardboard Settings Functions +JNI_SETTINGS_METHOD(jlong, nativeOnCreate) +(JNIEnv* /*env*/, jobject obj, jobject asset_mgr) { + return settings_jptr( + new settings::Settings(javaVm, obj, asset_mgr)); +} + +JNI_SETTINGS_METHOD(void, nativeSwitchViewer) +(JNIEnv* /*env*/, jobject /*obj*/, jlong native_app) { + settings_native(native_app)->SwitchViewer(); +} + +// Cardboard Settings Functions +JNI_MAIN_METHOD(jlong, nativeOnCreate) +(JNIEnv* /*env*/, jobject obj, jobject asset_mgr) { + return main_jptr( + new main::Main(javaVm, obj, asset_mgr)); +} + +JNI_MAIN_METHOD(void, nativeSwitchViewer) +(JNIEnv* /*env*/, jobject /*obj*/, jlong native_app) { + main_native(native_app)->SwitchViewer(); +} + +} // extern "C" diff --git a/paperscope/paperscope_launcher/src/cardboard_launcher.cc b/paperscope/paperscope_launcher/src/cardboard_launcher.cc new file mode 100644 index 00000000..5174310c --- /dev/null +++ b/paperscope/paperscope_launcher/src/cardboard_launcher.cc @@ -0,0 +1,658 @@ +/* + * Copyright 2024 Google LLC + * + * 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. + */ +#include "cardboard_launcher.h" + +#include +#include +#include + +#include +#include +#include +#include + +#include "cardboard.h" +#include "cardboard_strings.h" +#include "demo_icon.h" +#include "util.h" + +namespace cardboard_launcher { + +namespace { + +constexpr float kDefaultFloorHeight{-1.7f}; + +// 6 Hz cutoff frequency for the velocity filter of the head tracker. +constexpr int32_t kVelocityFilterCutoffFrequency{6}; + +constexpr uint64_t kPredictionTimeWithoutVsyncNanos{50000000}; + +// Angle threshold for determining whether the controller is pointing at the +// demo icon. +constexpr float kAngleLimit{0.1f}; + +// Distance between the demos. +constexpr float kDistanceBetweenDemos{0.7f}; + +// Vertical offset for the black square. +constexpr float kBlackSquareYOffset{2.2f}; + +// Distance from the user for the black square. +constexpr float kBlackSquareZOffset{-3.0f}; + +// Scale for the background when showing demos. +constexpr std::array kDemosBackgroundScale{3.1f, 1.1f, 1.0f}; + +// Scale for the background during the tutorial. +constexpr std::array kTutorialBackgroundScale{2.0f, 2.2f, 1.0f}; + +// Simple shaders to render .obj files without any lighting. +constexpr const char* kObjVertexShader = + R"glsl( + uniform mat4 u_MVP; + attribute vec4 a_Position; + attribute vec2 a_UV; + varying vec2 v_UV; + + void main() { + v_UV = a_UV; + gl_Position = u_MVP * a_Position; + })glsl"; + +constexpr const char* kObjFragmentShader = + R"glsl( + precision mediump float; + + uniform sampler2D u_Texture; + varying vec2 v_UV; + + void main() { + // The y coordinate of this sample's textures is reversed compared to + // what OpenGL expects, so we invert the y coordinate. + gl_FragColor = texture2D(u_Texture, vec2(v_UV.x, 1.0 - v_UV.y)); + })glsl"; + +constexpr const char* kVertexShader = R"glsl( + uniform mat4 u_MVP; + uniform mat4 u_Scale; + attribute vec4 a_Position; + void main () { + gl_Position = u_MVP * u_Scale * a_Position; + } +)glsl"; + +constexpr const char* kFragmentShader = R"glsl( + precision mediump float; + void main () { + gl_FragColor = vec4(0, 0, 0, 0.4); + } +)glsl"; + +// Button animation. +const std::vector kTutorialButtonAssets{ + "button_animation_0001.png", "button_animation_0002.png", + "button_animation_0003.png", "button_animation_0004.png", + "button_animation_0005.png", "button_animation_0006.png", + "button_animation_0007.png", "button_animation_0008.png", + "button_animation_0009.png", "button_animation_0010.png", + "button_animation_0011.png", "button_animation_0012.png", + "button_animation_0013.png", "button_animation_0014.png", + "button_animation_0015.png"}; + +// Tilt animation. +const std::vector kTutorialTiltAssets{ + "tilt_animation_0000.png", "tilt_animation_0001.png", + "tilt_animation_0002.png", "tilt_animation_0003.png", + "tilt_animation_0004.png", "tilt_animation_0005.png", + "tilt_animation_0006.png", "tilt_animation_0007.png", + "tilt_animation_0008.png", "tilt_animation_0009.png", + "tilt_animation_0010.png", "tilt_animation_0011.png", + "tilt_animation_0012.png", "tilt_animation_0013.png", + "tilt_animation_0014.png"}; + +} // anonymous namespace + +CardboardLauncher::CardboardLauncher(JavaVM* vm, jobject obj, + jobject asset_mgr_obj) + : head_tracker_{nullptr}, + lens_distortion_{nullptr}, + distortion_renderer_{nullptr}, + screen_params_changed_{false}, + device_params_changed_{false}, + screen_width_{0}, + screen_height_{0}, + depthRenderBuffer_{0}, + framebuffer_{0}, + texture_{0}, + obj_program_{0}, + obj_position_param_{0}, + obj_uv_param_{0}, + obj_model_view_projection_param_{0}, + should_launch_demo_{false}, + state_{State::SHOW_DEMOS}, + last_state_event_{StateEvent::NONE}, + tutorial_animator_{kTutorialButtonAssets, kTutorialTiltAssets, + &text_renderer_}, + black_background_mvp_param_{0}, + black_background_scale_param_{0} { + JNIEnv* env; + vm->GetEnv((void**)&env, JNI_VERSION_1_6); + java_asset_mgr_ = env->NewGlobalRef(asset_mgr_obj); + asset_mgr_ = AAssetManager_fromJava(env, asset_mgr_obj); + + Cardboard_initializeAndroid(vm, obj); + head_tracker_ = CardboardHeadTracker_create(); + CardboardHeadTracker_setLowPassFilter(head_tracker_, + kVelocityFilterCutoffFrequency); +} + +CardboardLauncher::~CardboardLauncher() { + CardboardHeadTracker_destroy(head_tracker_); + CardboardLensDistortion_destroy(lens_distortion_); + CardboardDistortionRenderer_destroy(distortion_renderer_); +} + +void CardboardLauncher::OnSurfaceCreated(JNIEnv* env) { + const int obj_vertex_shader = + cardboard_demo::LoadGLShader(GL_VERTEX_SHADER, kObjVertexShader); + const int obj_fragment_shader = + cardboard_demo::LoadGLShader(GL_FRAGMENT_SHADER, kObjFragmentShader); + + obj_program_ = glCreateProgram(); + glAttachShader(obj_program_, obj_vertex_shader); + glAttachShader(obj_program_, obj_fragment_shader); + glLinkProgram(obj_program_); + glUseProgram(obj_program_); + + cardboard_demo::CHECKGLERROR("Obj program"); + + obj_position_param_ = glGetAttribLocation(obj_program_, "a_Position"); + obj_uv_param_ = glGetAttribLocation(obj_program_, "a_UV"); + obj_model_view_projection_param_ = + glGetUniformLocation(obj_program_, "u_MVP"); + + cardboard_demo::CHECKGLERROR("Obj program params"); + + CARDBOARDDEMO_CHECK(background_.Initialize(obj_position_param_, obj_uv_param_, + "World.obj", asset_mgr_)); + CARDBOARDDEMO_CHECK( + background_tex_.Initialize(env, java_asset_mgr_, "home_mono.jpg")); + + text_renderer_.Init(); + text_renderer_.LoadFont(asset_mgr_, "Roboto-Regular.ttf"); + + const std::array demo_icon_data{ + DemoIconData{"tutorial", cardboard::GetLocalizedString( + cardboard::kStrTutorialDemoTitle, env)}, + // BEGIN-INTERNAL-EXHIBIT + // Hide the exhibit demo from being open sourced. Do not remove the + // guards. + DemoIconData{"exhibit", cardboard::GetLocalizedString( + cardboard::kStrExhibitDemoTitle, env)}, + // END-INTERNAL-EXHIBIT + DemoIconData{"mymovies", + cardboard::GetLocalizedString( + cardboard::kStrMyVideosAndroidDemoTitle, env)}, + DemoIconData{"photospheres", + cardboard::GetLocalizedString( + cardboard::kStrPhotoSphereAndroidDemoTitle, env)}}; + + demos_.clear(); + demos_.reserve(demo_icon_data.size()); + + // Create demos. + for (size_t i = 0; i < demo_icon_data.size(); ++i) { + // Demo meshes appear directly in front of user. + const float pos{((demo_icon_data.size() - 1) / 2.0f - i) * + kDistanceBetweenDemos}; + demos_.emplace_back(demo_icon_data.at(i), pos, &text_renderer_); + } + + tutorial_animator_.SetJNIEnv(env); + + LoadBlackBackground(); + LoadDemos(env); + LoadTutorial(env); + + cardboard_demo::CHECKGLERROR("OnSurfaceCreated"); +} + +void CardboardLauncher::LoadBlackBackground() { + const float vertices[] { + 0.5f, 0.5f, 0.0f, // top right + 0.5f, -0.5f, 0.0f, // bottom right + -0.5f, -0.5f, 0.0f, // bottom left + -0.5f, 0.5f, 0.0f // top left + }; + + const uint32_t indices[]{ + // note that we start from 0! + 0, 1, 3, // first triangle + 1, 2, 3 // second triangle + }; + + glGenBuffers(1, &black_background_vbo_); + glGenBuffers(1, &black_background_ebo_); + + glBindBuffer(GL_ARRAY_BUFFER, black_background_vbo_); + glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); + + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, black_background_ebo_); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, + GL_STATIC_DRAW); + + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + + const int32_t vertex_shader = + cardboard_demo::LoadGLShader(GL_VERTEX_SHADER, kVertexShader); + const int32_t fragment_shader = + cardboard_demo::LoadGLShader(GL_FRAGMENT_SHADER, kFragmentShader); + + black_background_program_id_ = glCreateProgram(); + glAttachShader(black_background_program_id_, vertex_shader); + glAttachShader(black_background_program_id_, fragment_shader); + cardboard_demo::CHECKGLERROR("LoadBlackBackground attach shaders"); + + glLinkProgram(black_background_program_id_); + cardboard_demo::CHECKGLERROR("LoadBlackBackground link program"); + + black_background_mvp_param_ = + glGetUniformLocation(black_background_program_id_, "u_MVP"); + black_background_scale_param_ = + glGetUniformLocation(black_background_program_id_, "u_Scale"); +} + +void CardboardLauncher::LoadDemos(JNIEnv* env) { + for (DemoIcon& demo : demos_) { + demo.LoadDemo(obj_position_param_, obj_uv_param_, asset_mgr_, env, + java_asset_mgr_); + } +} + +void CardboardLauncher::LoadTutorial(JNIEnv* env) { + tutorial_animator_.LoadAnimations(obj_position_param_, obj_uv_param_, + asset_mgr_, env, java_asset_mgr_); +} + +void CardboardLauncher::SetScreenParams(int width, int height) { + screen_width_ = width; + screen_height_ = height; + screen_params_changed_ = true; +} + +void CardboardLauncher::OnDrawFrame() { + if (!UpdateDeviceParams()) { + return; + } + + // Update Head Pose. + head_view_ = GetPose(); + + // Incorporate the height from the floor into the head_view. + head_view_ = head_view_ * cardboard_demo::GetTranslationMatrix( + {0.0f, kDefaultFloorHeight, 0.0f}); + + // Bind buffer. + glBindFramebuffer(GL_FRAMEBUFFER, framebuffer_); + + glEnable(GL_DEPTH_TEST); + glEnable(GL_CULL_FACE); + glDisable(GL_SCISSOR_TEST); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + // Draw eyes views. + for (int eye = 0; eye < 2; ++eye) { + glViewport(eye == kLeft ? 0 : screen_width_ / 2, 0, screen_width_ / 2, + screen_height_); + + const cardboard_demo::Matrix4x4 eye_matrix = + cardboard_demo::GetMatrixFromGlArray(eye_matrices_[eye]); + const cardboard_demo::Matrix4x4 eye_view = eye_matrix * head_view_; + + const cardboard_demo::Matrix4x4 projection_matrix = + cardboard_demo::GetMatrixFromGlArray(projection_matrices_[eye]); + + for (DemoIcon& demo : demos_) { + const cardboard_demo::Matrix4x4 model_view_demo = + eye_view * demo.GetModel(); + demo.SetModelViewProjection(projection_matrix * model_view_demo); + } + + if (state_ == State::TILT_TUTORIAL || state_ == State::BUTTON_TUTORIAL) { + tutorial_animator_.SetScreenPosition(eye_view, projection_matrix); + } + + model_view_projection_background_ = projection_matrix * eye_view; + const cardboard_demo::Matrix4x4 model_black_square = + cardboard_demo::GetTranslationMatrix({0.0f, kBlackSquareYOffset, + kBlackSquareZOffset}); + model_view_projection_black_square_ = + projection_matrix * eye_view * model_black_square; + + // Draw background and demos. + DrawWorld(); + + // Update State Machine. + UpdateState(); + } + + // Render. + CardboardDistortionRenderer_renderEyeToDisplay( + distortion_renderer_, /* target_display = */ 0, /* x = */ 0, /* y = */ 0, + screen_width_, screen_height_, &left_eye_texture_description_, + &right_eye_texture_description_); + + cardboard_demo::CHECKGLERROR("onDrawFrame"); +} + +void CardboardLauncher::OnTriggerEvent() { + // Check if the pointed demo represents the built-in tutorial. + if (IsPointingAtDemo(demos_.at(0).GetModel())) { + LOGD("Launch Tutorial"); + last_state_event_ = StateEvent::TUTORIAL_SELECTED; + } else { + for (size_t i = 1; i < demos_.size(); ++i) { + if (IsPointingAtDemo(demos_.at(i).GetModel()) && + state_ == State::SHOW_DEMOS) { + LOGD("Launching Demo %s", demos_.at(i).GetDemoName().c_str()); + should_launch_demo_ = true; + demo_to_launch_ = demos_.at(i).GetDemoName(); + } + } + } + + if (tutorial_animator_.IsNextClicked(head_view_, kAngleLimit)) { + last_state_event_ = StateEvent::TUTORIAL_NEXT_SELECTED; + } +} + +void CardboardLauncher::OnPause() { CardboardHeadTracker_pause(head_tracker_); } + +void CardboardLauncher::OnResume() { + CardboardHeadTracker_resume(head_tracker_); + + // Parameters may have changed. + device_params_changed_ = true; + + // Check for device parameters existence in external storage. If they're + // missing, we must scan a Cardboard QR code and save the obtained parameters. + uint8_t* buffer; + int size; + CardboardQrCode_getSavedDeviceParams(&buffer, &size); + if (size == 0) { + SwitchViewer(); + } + CardboardQrCode_destroy(buffer); +} + +void CardboardLauncher::SwitchViewer() { + CardboardQrCode_scanQrCodeAndSaveDeviceParams(); +} + +bool CardboardLauncher::UpdateDeviceParams() { + // Checks if screen or device parameters changed. + if (!screen_params_changed_ && !device_params_changed_) { + return true; + } + + // Get saved device parameters. + uint8_t* buffer; + int size; + CardboardQrCode_getSavedDeviceParams(&buffer, &size); + + // If there are no parameters saved yet, returns false. + if (size == 0) { + return false; + } + + CardboardLensDistortion_destroy(lens_distortion_); + lens_distortion_ = CardboardLensDistortion_create(buffer, size, screen_width_, + screen_height_); + + CardboardQrCode_destroy(buffer); + + GlSetup(); + + CardboardDistortionRenderer_destroy(distortion_renderer_); + const CardboardOpenGlEsDistortionRendererConfig config{kGlTexture2D}; + distortion_renderer_ = CardboardOpenGlEs2DistortionRenderer_create(&config); + + CardboardMesh left_mesh; + CardboardMesh right_mesh; + CardboardLensDistortion_getDistortionMesh(lens_distortion_, kLeft, + &left_mesh); + CardboardLensDistortion_getDistortionMesh(lens_distortion_, kRight, + &right_mesh); + + CardboardDistortionRenderer_setMesh(distortion_renderer_, &left_mesh, kLeft); + CardboardDistortionRenderer_setMesh(distortion_renderer_, &right_mesh, + kRight); + + // Get eye matrices. + CardboardLensDistortion_getEyeFromHeadMatrix(lens_distortion_, kLeft, + eye_matrices_[0]); + CardboardLensDistortion_getEyeFromHeadMatrix(lens_distortion_, kRight, + eye_matrices_[1]); + CardboardLensDistortion_getProjectionMatrix(lens_distortion_, kLeft, kZNear, + kZFar, projection_matrices_[0]); + CardboardLensDistortion_getProjectionMatrix(lens_distortion_, kRight, kZNear, + kZFar, projection_matrices_[1]); + + screen_params_changed_ = false; + device_params_changed_ = false; + + cardboard_demo::CHECKGLERROR("UpdateDeviceParams"); + + return true; +} + +void CardboardLauncher::GlSetup() { + LOGD("GL SETUP"); + + if (framebuffer_ != 0) { + GlTeardown(); + } + + // Create render texture. + glGenTextures(1, &texture_); + glBindTexture(GL_TEXTURE_2D, texture_); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, screen_width_, screen_height_, 0, + GL_RGB, GL_UNSIGNED_BYTE, 0); + + left_eye_texture_description_.texture = texture_; + left_eye_texture_description_.left_u = 0; + left_eye_texture_description_.right_u = 0.5; + left_eye_texture_description_.top_v = 1; + left_eye_texture_description_.bottom_v = 0; + + right_eye_texture_description_.texture = texture_; + right_eye_texture_description_.left_u = 0.5; + right_eye_texture_description_.right_u = 1; + right_eye_texture_description_.top_v = 1; + right_eye_texture_description_.bottom_v = 0; + + // Generate depth buffer to perform depth test. + glGenRenderbuffers(1, &depthRenderBuffer_); + glBindRenderbuffer(GL_RENDERBUFFER, depthRenderBuffer_); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, screen_width_, + screen_height_); + cardboard_demo::CHECKGLERROR("Create Render buffer"); + + // Create render demo icon. + glGenFramebuffers(1, &framebuffer_); + glBindFramebuffer(GL_FRAMEBUFFER, framebuffer_); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, + texture_, 0); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, + GL_RENDERBUFFER, depthRenderBuffer_); + + cardboard_demo::CHECKGLERROR("GlSetup"); +} + +void CardboardLauncher::GlTeardown() { + if (framebuffer_ == 0) { + return; + } + glDeleteRenderbuffers(1, &depthRenderBuffer_); + depthRenderBuffer_ = 0; + glDeleteFramebuffers(1, &framebuffer_); + framebuffer_ = 0; + glDeleteTextures(1, &texture_); + texture_ = 0; + + cardboard_demo::CHECKGLERROR("GlTeardown"); +} + +cardboard_demo::Matrix4x4 CardboardLauncher::GetPose() const { + std::array out_orientation; + std::array out_position; + CardboardHeadTracker_getPose( + head_tracker_, + cardboard_demo::GetBootTimeNano() + kPredictionTimeWithoutVsyncNanos, + kLandscapeLeft, &out_position.at(0), &out_orientation.at(0)); + return cardboard_demo::GetTranslationMatrix(out_position) * + cardboard_demo::Quatf::FromXYZW(&out_orientation.at(0)).ToMatrix(); +} + +void CardboardLauncher::DrawWorld() { DrawBackground(); } + +void CardboardLauncher::UpdateState() { + switch (state_) { + case State::SHOW_DEMOS: + DrawBlackBackgroundGL(kDemosBackgroundScale); + DrawDemos(); + // Check transition to button tutorial state. + if (last_state_event_ == StateEvent::TUTORIAL_SELECTED) { + state_ = State::BUTTON_TUTORIAL; + } + break; + case State::BUTTON_TUTORIAL: + DrawBlackBackgroundGL(kTutorialBackgroundScale); + tutorial_animator_.DisplayButtonAnimation( + obj_program_, obj_model_view_projection_param_); + // Check transition to tilt tutorial state. + if (last_state_event_ == StateEvent::TUTORIAL_NEXT_SELECTED) { + state_ = State::TILT_TUTORIAL; + } + break; + case State::TILT_TUTORIAL: + DrawBlackBackgroundGL(kTutorialBackgroundScale); + const float roll_angle_ = + atan2(head_view_.m[1][0], head_view_.m[0][0]) * 180.0 / M_PI; + if (roll_angle_ < -80.0f && roll_angle_ > -100.0f) { + last_state_event_ = StateEvent::DEVICE_IN_PORTRAIT; + } + tutorial_animator_.DisplayTiltAnimation(obj_program_, + obj_model_view_projection_param_); + // Check transition to show demos state. + if (last_state_event_ == StateEvent::DEVICE_IN_PORTRAIT) { + state_ = State::SHOW_DEMOS; + } + break; + } +} + +void CardboardLauncher::DrawDemos() { + glUseProgram(obj_program_); + + for (DemoIcon& demo : demos_) { + const std::array target_array { + demo.GetModelViewProjection().ToGlArray()}; + glUniformMatrix4fv(obj_model_view_projection_param_, 1, GL_FALSE, + target_array.data()); + + demo.Draw(); + demo.SetPointingAtDemo(IsPointingAtDemo(demo.GetModel())); + } + cardboard_demo::CHECKGLERROR("DrawDemos"); +} + +void CardboardLauncher::DrawBackground() { + glUseProgram(obj_program_); + + std::array background_array = + model_view_projection_background_.ToGlArray(); + glUniformMatrix4fv(obj_model_view_projection_param_, 1, GL_FALSE, + background_array.data()); + + background_tex_.Bind(); + background_.Draw(); + + cardboard_demo::CHECKGLERROR("DrawBackground"); +} + +void CardboardLauncher::DrawBlackBackgroundGL( + const std::array& scale) { + glDisable(GL_DEPTH_TEST); + glDisable(GL_CULL_FACE); + + glUseProgram(black_background_program_id_); + + const std::array target_array{ + model_view_projection_black_square_.ToGlArray()}; + glUniformMatrix4fv(black_background_mvp_param_, 1, GL_FALSE, + target_array.data()); + + cardboard_demo::Matrix4x4 scale_matrix = + cardboard_demo::GetScaleMatrix(scale); + const std::array scale_array{scale_matrix.ToGlArray()}; + glUniformMatrix4fv(black_background_scale_param_, 1, GL_FALSE, + scale_array.data()); + + glBindBuffer(GL_ARRAY_BUFFER, black_background_vbo_); + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), nullptr); + glEnableVertexAttribArray(0); + + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, black_background_ebo_); + + glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr); + + glDisableVertexAttribArray(0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + glEnable(GL_CULL_FACE); + glEnable(GL_DEPTH_TEST); +} + +bool CardboardLauncher::IsPointingAtDemo( + const cardboard_demo::Matrix4x4& model_demo) { + // Compute vectors pointing towards the reticle and towards the demo icon in + // head space. + cardboard_demo::Matrix4x4 head_from_demo = head_view_ * model_demo; + const std::array unit_quaternion {0.f, 0.f, 0.f, 1.f}; + const std::array point_vector {0.f, 0.f, -1.f, 0.f}; + const std::array demo_vector { head_from_demo * unit_quaternion }; + + const float angle { + cardboard_demo::AngleBetweenVectors(point_vector, demo_vector)}; + return angle < kAngleLimit; +} + +bool CardboardLauncher::ShouldLaunchDemo() { return should_launch_demo_; } + +std::string CardboardLauncher::DemoToLaunch() { return demo_to_launch_; } + +void CardboardLauncher::DemoLaunched() { should_launch_demo_ = false; } + +} // namespace cardboard_launcher diff --git a/paperscope/paperscope_launcher/src/cardboard_launcher.h b/paperscope/paperscope_launcher/src/cardboard_launcher.h new file mode 100644 index 00000000..4c51d975 --- /dev/null +++ b/paperscope/paperscope_launcher/src/cardboard_launcher.h @@ -0,0 +1,287 @@ +/* + * Copyright 2024 Google LLC + * + * 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. + */ +#ifndef PAPERSCOPE_LAUNCHER_SRC_CARDBOARD_LAUNCHER_H_ +#define PAPERSCOPE_LAUNCHER_SRC_CARDBOARD_LAUNCHER_H_ + +#include +#include + +#include + +#include +#include "cardboard.h" +#include "demo_icon.h" +#include "text_renderer.h" +#include "tutorial_animator.h" +#include "util.h" + +namespace cardboard_launcher { + +/** + * This is a sample app for the Cardboard SDK. It loads a simple environment and + * objects that you can click on. + */ +class CardboardLauncher { + public: + /** + * Creates a DemoCardboardDemo. + * + * @param vm JavaVM pointer. + * @param obj Android activity object. + * @param asset_mgr_obj The asset manager object. + */ + CardboardLauncher(JavaVM* vm, jobject obj, jobject asset_mgr_obj); + + ~CardboardLauncher(); + + /** + * Initializes any GL-related objects. This should be called on the rendering + * thread with a valid GL context. + * + * @param env The JNI environment. + */ + void OnSurfaceCreated(JNIEnv* env); + + /** + * Sets screen parameters. + * + * @param width Screen width + * @param height Screen height + */ + void SetScreenParams(int width, int height); + + /** + * Draws the scene. This should be called on the rendering thread. + */ + void OnDrawFrame(); + + /** + * Hides the target object if it's being targeted. + */ + void OnTriggerEvent(); + + /** + * Pauses head tracking. + */ + void OnPause(); + + /** + * Resumes head tracking. + */ + void OnResume(); + + /** + * Allows user to switch viewer. + */ + void SwitchViewer(); + + /** + * Checks if the demo should be launched. + */ + bool ShouldLaunchDemo(); + + /** + * Forwards the demo name to launch. + */ + std::string DemoToLaunch(); + + /** + * Resets the should_launch_demo flag after the demo is launched. + */ + void DemoLaunched(); + + private: + /** + * Current state of the launcher. + */ + enum class State { SHOW_DEMOS, BUTTON_TUTORIAL, TILT_TUTORIAL }; + + /** + * State events of the launcher. + */ + enum class StateEvent { + TUTORIAL_SELECTED, + TUTORIAL_NEXT_SELECTED, + DEVICE_IN_PORTRAIT, + NONE + }; + + /** + * Default near clip plane z-axis coordinate. + */ + static constexpr float kZNear = 0.1f; + + /** + * Default far clip plane z-axis coordinate. + */ + static constexpr float kZFar = 100.f; + + /** + * Updates device parameters, if necessary. + * + * @return true if device parameters were successfully updated. + */ + bool UpdateDeviceParams(); + + /** + * Initializes GL environment. + */ + void GlSetup(); + + /** + * Deletes GL environment. + */ + void GlTeardown(); + + /** + * Gets head's pose as a 4x4 matrix. + * + * @return matrix containing head's pose. + */ + cardboard_demo::Matrix4x4 GetPose() const; + + /** + * Draws all world-space objects for the given eye. + */ + void DrawWorld(); + + /** + * Draws the background. + */ + void DrawBackground(); + + /** + * Draws once demo. + */ + void DrawDemos(); + + /** + * Checks if user is pointing or looking at the demo mesh by calculating + * whether the angle between the user's gaze and the vector pointing towards + * the mesh is lower than some threshold. + * + * @return true if the user is pointing at the demo mesh. + */ + bool IsPointingAtDemo(const cardboard_demo::Matrix4x4& model_demo); + + /** + * Checks if user is pointing or looking at the next button by calculating + * whether the angle between the user's gaze and the vector pointing towards + * the button is lower than some threshold. + * + * @return true if the user is pointing at the next button. + */ + bool IsPointingAtNextButton(); + + /** + * Loads the demo meshes. + * + * @param env The JNI environment. + */ + void LoadDemos(JNIEnv* env); + + /** + * Loads the tutorial animations. + * + * @param env The JNI environment. + */ + void LoadTutorial(JNIEnv* env); + + /** + * Shows the button tutorial. + */ + void ShowButtonTutorial(); + + /** + * Updates the state of the launcher state machine. + */ + void UpdateState(); + + /** + * Loads the black background and its parameters. + */ + void LoadBlackBackground(); + + /** + * Draws the black background with the given scale. + * + * @param scale The scale of the black background. + */ + void DrawBlackBackgroundGL(const std::array& scale); + + jobject java_asset_mgr_; + AAssetManager* asset_mgr_; + + CardboardHeadTracker* head_tracker_; + CardboardLensDistortion* lens_distortion_; + CardboardDistortionRenderer* distortion_renderer_; + + CardboardEyeTextureDescription left_eye_texture_description_; + CardboardEyeTextureDescription right_eye_texture_description_; + + bool screen_params_changed_; + bool device_params_changed_; + int screen_width_; + int screen_height_; + + float projection_matrices_[2][16]; + float eye_matrices_[2][16]; + + GLuint depthRenderBuffer_; // depth buffer + GLuint framebuffer_; // framebuffer object + GLuint texture_; // distortion texture + + GLuint obj_program_; + GLuint obj_position_param_; + GLuint obj_uv_param_; + GLuint obj_model_view_projection_param_; + + cardboard_demo::Matrix4x4 head_view_; + + std::vector demos_; + + cardboard_demo::Matrix4x4 model_view_projection_background_; + + cardboard_demo::TexturedMesh background_; + cardboard_demo::Texture background_tex_; + + // Indicates whether to launch the demo. + bool should_launch_demo_; + + // Indicates the demo to launch. + std::string demo_to_launch_; + + // Indicates the current state of the launcher. + State state_; + // Indicates the last state event of the launcher. + StateEvent last_state_event_; + + // Tutorial animations. + TutorialAnimator tutorial_animator_; + + GLuint black_background_vbo_; + GLuint black_background_ebo_; + GLuint black_background_program_id_; + GLuint black_background_mvp_param_; + GLuint black_background_scale_param_; + + cardboard_demo::Matrix4x4 model_view_projection_black_square_; + cardboard_demo::TextRenderer text_renderer_; +}; + +} // namespace cardboard_launcher + +#endif // PAPERSCOPE_LAUNCHER_SRC_CARDBOARD_LAUNCHER_H_ diff --git a/paperscope/paperscope_launcher/src/demo_icon.cc b/paperscope/paperscope_launcher/src/demo_icon.cc new file mode 100644 index 00000000..1a8a0d74 --- /dev/null +++ b/paperscope/paperscope_launcher/src/demo_icon.cc @@ -0,0 +1,74 @@ +/* + * Copyright 2024 Google LLC + * + * 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. + */ +#include "demo_icon.h" + +#include + +#include "util.h" + +namespace cardboard_launcher { + +namespace { +constexpr float kTextScale{0.002f}; +constexpr std::array kTextTutorialScale{-kTextScale, + kTextScale, + 1.0f}; +const cardboard_demo::Matrix4x4 kTextScaleMatrix{ + cardboard_demo::GetScaleMatrix(kTextTutorialScale)}; +constexpr float kTextYOffset{-0.3f}; +} // namespace + +void DemoIcon::LoadDemo(GLuint obj_position_param, GLuint obj_uv_param, + AAssetManager* asset_mgr, JNIEnv* env, + jobject java_asset_mgr) { + CARDBOARDDEMO_CHECK(demo_mesh_.Initialize(obj_position_param, obj_uv_param, + "IconMesh.obj", asset_mgr)); + CARDBOARDDEMO_CHECK(demo_not_selected_texture_.Initialize( + env, java_asset_mgr, demo_icon_data_.name + "-bw.png")); + CARDBOARDDEMO_CHECK(demo_selected_texture_.Initialize( + env, java_asset_mgr, demo_icon_data_.name + "-color.png")); +} + +void DemoIcon::Draw() const { + if (is_pointing_at_demo_) { + demo_selected_texture_.Bind(); + } else { + demo_not_selected_texture_.Bind(); + } + demo_mesh_.Draw(); + + if (is_pointing_at_demo_) { + DrawDemoName(); + } + + cardboard_demo::CHECKGLERROR("DrawDemoExhibit"); +} + +void DemoIcon::DrawDemoName() const { + const float x_offset{ + text_renderer_->GetTextWidth(demo_icon_data_.display_name) * 0.5f}; + + cardboard_demo::Matrix4x4 translation_matrix{ + cardboard_demo::GetTranslationMatrix({x_offset * kTextScale, + kTextYOffset, + 0.0f})}; + + const cardboard_demo::Matrix4x4 model_view_projection = + GetModelViewProjection() * translation_matrix * kTextScaleMatrix; + text_renderer_->RenderText(demo_icon_data_.display_name, + model_view_projection); +} +} // namespace cardboard_launcher diff --git a/paperscope/paperscope_launcher/src/demo_icon.h b/paperscope/paperscope_launcher/src/demo_icon.h new file mode 100644 index 00000000..a67a6c8b --- /dev/null +++ b/paperscope/paperscope_launcher/src/demo_icon.h @@ -0,0 +1,125 @@ + +/* + * Copyright 2024 Google LLC + * + * 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. + */ +#ifndef PAPERSCOPE_LAUNCHER_SRC_DEMO_ICON_H_ +#define PAPERSCOPE_LAUNCHER_SRC_DEMO_ICON_H_ + +#include + +#include +#include + +#include "text_renderer.h" +#include "util.h" + +namespace cardboard_launcher { + +struct DemoIconData { + std::string name; + std::string display_name; +}; + +class DemoIcon { + public: + /** + * Constructor for a demo icon. + * + * @param demo_name The name of the demo. + * @param pos The position of the demo. + */ + DemoIcon(const cardboard_launcher::DemoIconData& demo_icon_data, + const float pos, cardboard_demo::TextRenderer* const text_renderer) + : demo_icon_data_{demo_icon_data}, text_renderer_{text_renderer} { + // Rotate the demo icon and place it in front of the user. + const float yaw{180.0f * M_PI / 180.0f}; + model_demo_ = cardboard_demo::GetTranslationMatrix({-pos, 2.2f, -3.0f}) + * cardboard_demo::GetYawRotationMatrix(yaw); + } + + /** + * Returns the name of the demo. + */ + std::string GetDemoName() const { return demo_icon_data_.name; } + + /** + * Returns the model of the demo. + */ + cardboard_demo::Matrix4x4 GetModel() const { return model_demo_; } + + /** + * Returns the model view projection of the demo. + */ + cardboard_demo::Matrix4x4 GetModelViewProjection() const { + return modelview_projection_demo_; + } + + /** + * Sets the model of the demo. + * + * @param model The model of the demo. + */ + void SetModel(const cardboard_demo::Matrix4x4& model) { model_demo_ = model; } + + /** + * Sets the model view projection of the demo. + * + * @param modelview_projection The model view projection of the demo. + */ + void SetModelViewProjection( + const cardboard_demo::Matrix4x4& modelview_projection) { + modelview_projection_demo_ = modelview_projection; + } + + /** + * Sets whether the user is pointing at the demo. + * + * @param is_pointing_at_demo Whether the user is pointing at the demo. + */ + void SetPointingAtDemo(bool is_pointing_at_demo) { + is_pointing_at_demo_ = is_pointing_at_demo; + } + + /** + * Loads the demo mesh. + */ + void LoadDemo(GLuint obj_position_param_, GLuint obj_uv_param_, + AAssetManager* asset_mgr_, JNIEnv* env, + jobject java_asset_mgr_); + + /** + * Draws the demo mesh. + */ + void Draw() const; + + void DrawDemoName() const; + + private: + cardboard_launcher::DemoIconData demo_icon_data_; + cardboard_demo::Matrix4x4 model_demo_; + cardboard_demo::Matrix4x4 modelview_projection_demo_; + + cardboard_demo::TexturedMesh demo_mesh_; + cardboard_demo::Texture demo_not_selected_texture_; + cardboard_demo::Texture demo_selected_texture_; + + cardboard_demo::TextRenderer* const text_renderer_; + + bool is_pointing_at_demo_; +}; + +} // namespace cardboard_launcher + +#endif // PAPERSCOPE_LAUNCHER_SRC_DEMO_ICON_H_ diff --git a/paperscope/paperscope_launcher/src/magic_window.cc b/paperscope/paperscope_launcher/src/magic_window.cc new file mode 100644 index 00000000..dfa3610d --- /dev/null +++ b/paperscope/paperscope_launcher/src/magic_window.cc @@ -0,0 +1,188 @@ +/* + * Copyright 2024 Google LLC + * + * 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. + */ +#include "magic_window.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include "cardboard.h" +#include "util/matrix_4x4.h" +#include "util.h" + +namespace magic_window { + +namespace { + +constexpr float kDefaultFloorHeight = -1.7f; + +// 6 Hz cutoff frequency for the velocity filter of the head tracker. +constexpr int kVelocityFilterCutoffFrequency = 6; + +constexpr uint64_t kPredictionTimeWithoutVsyncNanos = 50000000; + +// Simple shaders to render .obj files without any lighting. +constexpr const char* kObjVertexShader = + R"glsl( + uniform mat4 u_MVP; + attribute vec4 a_Position; + attribute vec2 a_UV; + varying vec2 v_UV; + + void main() { + v_UV = a_UV; + gl_Position = u_MVP * a_Position; + })glsl"; + +constexpr const char* kObjFragmentShader = + R"glsl( + precision mediump float; + + uniform sampler2D u_Texture; + varying vec2 v_UV; + + void main() { + // The y coordinate of this sample's textures is reversed compared to + // what OpenGL expects, so we invert the y coordinate. + gl_FragColor = texture2D(u_Texture, vec2(v_UV.x, 1.0 - v_UV.y)); + })glsl"; +} // anonymous namespace + +MagicWindow::MagicWindow(JavaVM* vm, jobject obj, jobject asset_mgr_obj) + : head_tracker_(nullptr), + screen_width_(0), + screen_height_(0), + obj_program_(0), + obj_model_view_projection_param_(0) { + JNIEnv* env; + vm->GetEnv((void**)&env, JNI_VERSION_1_6); + java_asset_mgr_ = env->NewGlobalRef(asset_mgr_obj); + asset_mgr_ = AAssetManager_fromJava(env, asset_mgr_obj); + + Cardboard_initializeAndroid(vm, obj); + head_tracker_ = CardboardHeadTracker_create(); + CardboardHeadTracker_setLowPassFilter(head_tracker_, + kVelocityFilterCutoffFrequency); +} + +MagicWindow::~MagicWindow() { CardboardHeadTracker_destroy(head_tracker_); } + +void MagicWindow::OnSurfaceCreated(JNIEnv* env) { + const int obj_vertex_shader = + cardboard_demo::LoadGLShader(GL_VERTEX_SHADER, kObjVertexShader); + const int obj_fragment_shader = + cardboard_demo::LoadGLShader(GL_FRAGMENT_SHADER, kObjFragmentShader); + + obj_program_ = glCreateProgram(); + glAttachShader(obj_program_, obj_vertex_shader); + glAttachShader(obj_program_, obj_fragment_shader); + glLinkProgram(obj_program_); + glUseProgram(obj_program_); + + GLuint obj_position_param = glGetAttribLocation(obj_program_, "a_Position"); + GLuint obj_uv_param = glGetAttribLocation(obj_program_, "a_UV"); + obj_model_view_projection_param_ = + glGetUniformLocation(obj_program_, "u_MVP"); + + CARDBOARDDEMO_CHECK(background_.Initialize(obj_position_param, obj_uv_param, + "World.obj", asset_mgr_)); + CARDBOARDDEMO_CHECK( + background_tex_.Initialize(env, java_asset_mgr_, "home_mono.jpg")); +} + +void MagicWindow::SetScreenParams(int width, int height) { + screen_width_ = width; + screen_height_ = height; + SetFov(); + glViewport(0, 0, screen_width_, screen_height_); + glEnable(GL_CULL_FACE); + glCullFace(GL_BACK); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); +} + +void MagicWindow::OnDrawFrame() { + // Update Head Pose. + cardboard_demo::Matrix4x4 head_view = GetPose(); + + // Incorporate the height from the floor into the head_view. + head_view = head_view * cardboard_demo::GetTranslationMatrix( + {0.0f, kDefaultFloorHeight, 0.0f}); + + const cardboard_demo::Matrix4x4 projection_matrix = + cardboard_demo::GetMatrixFromGlArray(magic_window_projection_matrix_); + + model_view_projection_background_ = projection_matrix * head_view; + + // Draw background. + DrawBackground(); +} + +void MagicWindow::OnPause() { CardboardHeadTracker_pause(head_tracker_); } + +void MagicWindow::ScanViewer() { + CardboardQrCode_scanQrCodeAndSaveDeviceParams(); +} + +bool MagicWindow::HasSavedDeviceParams() { + // Get saved device parameters. + uint8_t* buffer; + int size; + CardboardQrCode_getSavedDeviceParams(&buffer, &size); + + return size > 0; +} + +void MagicWindow::OnResume() { CardboardHeadTracker_resume(head_tracker_); } + +void MagicWindow::SetFov() { + float ar = (float)screen_width_ / screen_height_; + float fov_h = 2 * atan(tan(kFov_v / 2) * ar); + std::array magic_window_fov = {fov_h, fov_h, kFov_v, kFov_v}; + cardboard::Matrix4x4::Perspective(magic_window_fov, kZNear, kZFar) + .ToArray(magic_window_projection_matrix_); +} + +cardboard_demo::Matrix4x4 MagicWindow::GetPose() { + std::array out_orientation; + std::array out_position; + CardboardHeadTracker_getPose( + head_tracker_, + cardboard_demo::GetBootTimeNano() + kPredictionTimeWithoutVsyncNanos, + kPortrait, &out_position[0], &out_orientation[0]); + return cardboard_demo::GetTranslationMatrix(out_position) * + cardboard_demo::Quatf::FromXYZW(&out_orientation[0]).ToMatrix(); +} + +void MagicWindow::DrawBackground() { + glClearColor(.0f, .0f, .0f, 1.0f); + glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); + + std::array background_array = + model_view_projection_background_.ToGlArray(); + glUniformMatrix4fv(obj_model_view_projection_param_, 1, GL_FALSE, + background_array.data()); + + background_tex_.Bind(); + background_.Draw(); +} + +} // namespace magic_window diff --git a/paperscope/paperscope_launcher/src/magic_window.h b/paperscope/paperscope_launcher/src/magic_window.h new file mode 100644 index 00000000..1cc86e7e --- /dev/null +++ b/paperscope/paperscope_launcher/src/magic_window.h @@ -0,0 +1,140 @@ +/* + * Copyright 2024 Google LLC + * + * 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. + */ +#ifndef PAPERSCOPE_LAUNCHER_SRC_MAGIC_WINDOW_H_ +#define PAPERSCOPE_LAUNCHER_SRC_MAGIC_WINDOW_H_ + +#include +#include + +#include +#include "cardboard.h" +#include "util.h" + +namespace magic_window { + +/** + * This is a magic window of the sample app for the Cardboard SDK. It shows a + * full-screen monoscopic view of a sample VR world. + */ +class MagicWindow { + public: + /** + * Creates the Magic Window of the sample app. + * + * @param vm JavaVM pointer. + * @param obj Android activity object. + * @param asset_mgr_obj The asset manager object. + */ + MagicWindow(JavaVM* vm, jobject obj, jobject asset_mgr_obj); + + ~MagicWindow(); + + /** + * Initializes any GL-related objects. This should be called on the rendering + * thread with a valid GL context. + * + * @param env The JNI environment. + */ + void OnSurfaceCreated(JNIEnv* env); + + /** + * Sets screen parameters. + * + * @param width Screen width + * @param height Screen height + */ + void SetScreenParams(int width, int height); + + /** + * Draws the scene. This should be called on the rendering thread. + */ + void OnDrawFrame(); + + /** + * Resumes head tracking. + */ + void OnResume(); + + /** + * Pauses head tracking. + */ + void OnPause(); + + /** + * Allows user to scan a Cardboard viewer. + */ + void ScanViewer(); + + /** + * Checks if a Cardboard viewer has previously been scanned and its parameters + * saved. + */ + bool HasSavedDeviceParams(); + + private: + /** + * Default near clip plane z-axis coordinate. + */ + static constexpr float kZNear = 0.1f; + + /** + * Default far clip plane z-axis coordinate. + */ + static constexpr float kZFar = 100.f; + + /** + * Default vertical FOV set to 45 degrees. + */ + static constexpr float kFov_v = 0.785398f; + + /** + * Gets head's pose as a 4x4 matrix. + * + * @return matrix containing head's pose. + */ + cardboard_demo::Matrix4x4 GetPose(); + + /** + * Initializes the FOV settings for the Magic Window. + */ + void SetFov(); + + /** + * Draws the background. + */ + void DrawBackground(); + + jobject java_asset_mgr_; + AAssetManager* asset_mgr_; + + CardboardHeadTracker* head_tracker_; + + int screen_width_; + int screen_height_; + + float magic_window_projection_matrix_[16]; + + GLuint obj_program_; + GLuint obj_model_view_projection_param_; + + cardboard_demo::Matrix4x4 model_view_projection_background_; + cardboard_demo::TexturedMesh background_; + cardboard_demo::Texture background_tex_; +}; + +} // namespace magic_window + +#endif // PAPERSCOPE_LAUNCHER_SRC_MAGIC_WINDOW_H_ diff --git a/paperscope/paperscope_launcher/src/main.cc b/paperscope/paperscope_launcher/src/main.cc new file mode 100644 index 00000000..0935a70f --- /dev/null +++ b/paperscope/paperscope_launcher/src/main.cc @@ -0,0 +1,43 @@ +/* + * Copyright 2025 Google LLC + * + * 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. + */ +#include "main.h" + +#include +#include +#include +#include + + +#include "cardboard.h" + +namespace main { + +Main::Main(JavaVM* vm, jobject obj, jobject asset_mgr_obj) { + JNIEnv* env; + vm->GetEnv((void**)&env, JNI_VERSION_1_6); + java_asset_mgr_ = env->NewGlobalRef(asset_mgr_obj); + asset_mgr_ = AAssetManager_fromJava(env, asset_mgr_obj); + + Cardboard_initializeAndroid(vm, obj); +} + +Main::~Main() {} + +void Main::SwitchViewer() { + CardboardQrCode_scanQrCodeAndSaveDeviceParams(); +} + +} // namespace main diff --git a/paperscope/paperscope_launcher/src/main.h b/paperscope/paperscope_launcher/src/main.h new file mode 100644 index 00000000..c1ca1cc6 --- /dev/null +++ b/paperscope/paperscope_launcher/src/main.h @@ -0,0 +1,57 @@ +/* + * Copyright 2025 Google LLC + * + * 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. + */ +#ifndef PAPERSCOPE_LAUNCHER_SRC_MAIN_H_ +#define PAPERSCOPE_LAUNCHER_SRC_MAIN_H_ + +#include +#include + +#include "cardboard.h" +#include "util.h" + + +namespace main { + +/** + * Handles the native interface calls for the Main Activity of the Cardboard + * sample app. + */ +class Main { + public: + /** + * Creates the Native interface for the Main Activity of the sample app. + * + * @param vm JavaVM pointer. + * @param obj Android activity object. + * @param asset_mgr_obj The asset manager object. + */ + Main(JavaVM* vm, jobject obj, jobject asset_mgr_obj); + + ~Main(); + + /** + * Allows user to scan a Cardboard viewer. + */ + void SwitchViewer(); + + private: + jobject java_asset_mgr_; + AAssetManager* asset_mgr_; +}; + +} // namespace main + +#endif // PAPERSCOPE_LAUNCHER_SRC_MAIN_H_ diff --git a/paperscope/paperscope_launcher/src/settings.cc b/paperscope/paperscope_launcher/src/settings.cc new file mode 100644 index 00000000..d9f8fc8e --- /dev/null +++ b/paperscope/paperscope_launcher/src/settings.cc @@ -0,0 +1,43 @@ +/* + * Copyright 2025 Google LLC + * + * 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. + */ +#include "settings.h" + +#include +#include +#include +#include + + +#include "cardboard.h" + +namespace settings { + +Settings::Settings(JavaVM* vm, jobject obj, jobject asset_mgr_obj) { + JNIEnv* env; + vm->GetEnv((void**)&env, JNI_VERSION_1_6); + java_asset_mgr_ = env->NewGlobalRef(asset_mgr_obj); + asset_mgr_ = AAssetManager_fromJava(env, asset_mgr_obj); + + Cardboard_initializeAndroid(vm, obj); +} + +Settings::~Settings() {} + +void Settings::SwitchViewer() { + CardboardQrCode_scanQrCodeAndSaveDeviceParams(); +} + +} // namespace settings diff --git a/paperscope/paperscope_launcher/src/settings.h b/paperscope/paperscope_launcher/src/settings.h new file mode 100644 index 00000000..55e8d03d --- /dev/null +++ b/paperscope/paperscope_launcher/src/settings.h @@ -0,0 +1,57 @@ +/* + * Copyright 2025 Google LLC + * + * 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. + */ +#ifndef PAPERSCOPE_LAUNCHER_SRC_SETTINGS_H_ +#define PAPERSCOPE_LAUNCHER_SRC_SETTINGS_H_ + +#include +#include + +#include "cardboard.h" +#include "util.h" + + +namespace settings { + +/** + * Handles the native interface calls for the Settings Activity of the Cardboard + * sample app. + */ +class Settings { + public: + /** + * Creates the Native interface for the Settings Activity of the sample app. + * + * @param vm JavaVM pointer. + * @param obj Android activity object. + * @param asset_mgr_obj The asset manager object. + */ + Settings(JavaVM* vm, jobject obj, jobject asset_mgr_obj); + + ~Settings(); + + /** + * Allows user to scan a Cardboard viewer. + */ + void SwitchViewer(); + + private: + jobject java_asset_mgr_; + AAssetManager* asset_mgr_; +}; + +} // namespace settings + +#endif // PAPERSCOPE_LAUNCHER_SRC_SETTINGS_H_ diff --git a/paperscope/paperscope_launcher/src/text_renderer.cc b/paperscope/paperscope_launcher/src/text_renderer.cc new file mode 100644 index 00000000..179fbd60 --- /dev/null +++ b/paperscope/paperscope_launcher/src/text_renderer.cc @@ -0,0 +1,372 @@ +#include "text_renderer.h" + +#include +#include + +#include "util.h" + +namespace cardboard_demo { +namespace { + +constexpr const char* kTextVertexShader = R"glsl( + uniform mat4 u_MVP; + attribute vec4 a_Vertex; // vec4(pos.x, pos.y, tex.x, tex.y) + varying vec2 v_TexCoords; + + void main() { + gl_Position = u_MVP * vec4(a_Vertex.xy, 0.0, 1.0); + v_TexCoords = a_Vertex.zw; + })glsl"; + +constexpr const char* kTextFragmentShader = R"glsl( + precision mediump float; + varying vec2 v_TexCoords; + uniform sampler2D u_Texture; + uniform vec3 u_TextColor; + + void main() { + // Use the red channel of the texture (luminance) as alpha. + float alpha = texture2D(u_Texture, v_TexCoords).r; + gl_FragColor = vec4(u_TextColor, alpha); + })glsl"; + +// Converts a UTF-8 encoded string to a UTF-32 encoded string. +std::u32string Utf8ToUtf32(const std::string& s) { + std::u32string result; + if (s.empty()) { + return result; + } + result.reserve(s.length()); + + for (size_t i = 0; i < s.length();) { + char32_t code_point; + const unsigned char c = s[i]; + + if (c < 0x80) { // 1-byte sequence + code_point = c; + i += 1; + } else if ((c & 0xE0) == 0xC0) { // 2-byte sequence + if (i + 1 >= s.length()) break; + code_point = ((c & 0x1F) << 6) | (s[i + 1] & 0x3F); + i += 2; + } else if ((c & 0xF0) == 0xE0) { // 3-byte sequence + if (i + 2 >= s.length()) break; + code_point = + ((c & 0x0F) << 12) | ((s[i + 1] & 0x3F) << 6) | (s[i + 2] & 0x3F); + i += 3; + } else if ((c & 0xF8) == 0xF0) { // 4-byte sequence + if (i + 3 >= s.length()) break; + code_point = ((c & 0x07) << 18) | ((s[i + 1] & 0x3F) << 12) | + ((s[i + 2] & 0x3F) << 6) | (s[i + 3] & 0x3F); + i += 4; + } else { + // Invalid UTF-8 sequence, skip this byte. + i += 1; + continue; + } + result.push_back(code_point); + } + return result; +} +} // namespace + +TextRenderer::TextRenderer() + : ft_library_{nullptr}, + ft_face_{nullptr}, + vbo_{0}, + shader_program_{0}, + mvp_param_{-1}, + text_color_param_{-1}, + vertex_param_{-1} { + if (FT_Init_FreeType(&ft_library_)) { + __android_log_print(ANDROID_LOG_ERROR, "TextRenderer", + "Could not initialize FreeType library"); + } +} + +TextRenderer::~TextRenderer() { + if (ft_face_) { + FT_Done_Face(ft_face_); + } + if (ft_library_) { + FT_Done_FreeType(ft_library_); + } + + for (auto const& [codepoint, character] : characters_) { + glDeleteTextures(1, &character.texture_id); + } +} + +void TextRenderer::Init() { + const GLuint vertex_shader{ + cardboard_demo::LoadGLShader(GL_VERTEX_SHADER, kTextVertexShader)}; + const GLuint fragment_shader{ + cardboard_demo::LoadGLShader(GL_FRAGMENT_SHADER, kTextFragmentShader)}; + + shader_program_ = glCreateProgram(); + glAttachShader(shader_program_, vertex_shader); + glAttachShader(shader_program_, fragment_shader); + glLinkProgram(shader_program_); + + mvp_param_ = glGetUniformLocation(shader_program_, "u_MVP"); + text_color_param_ = glGetUniformLocation(shader_program_, "u_TextColor"); + vertex_param_ = glGetAttribLocation(shader_program_, "a_Vertex"); + + glGenBuffers(1, &vbo_); + glBindBuffer(GL_ARRAY_BUFFER, vbo_); + glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 6 * 4, nullptr, + GL_DYNAMIC_DRAW); + + glBindBuffer(GL_ARRAY_BUFFER, 0); + + // Invalidate all textures in case of GL context loss. + for (auto& character : characters_) { + character.second.texture_id = 0; + } + cardboard_demo::CHECKGLERROR("TextRenderer::Init"); +} + +bool TextRenderer::LoadFont(AAssetManager* asset_mgr, + const std::string& font_path) { + if (asset_mgr == nullptr) { + __android_log_print(ANDROID_LOG_ERROR, "TextRenderer", + "Asset manager is null"); + return false; + } + if (!ft_library_) { + __android_log_print(ANDROID_LOG_ERROR, "TextRenderer", + "FreeType library is not initialized"); + return false; + } + + AAsset* asset{ + AAssetManager_open(asset_mgr, font_path.c_str(), AASSET_MODE_BUFFER)}; + if (!asset) { + __android_log_print(ANDROID_LOG_ERROR, "TextRenderer", + "Could not open font asset"); + return false; + } + + const void* font_buffer{AAsset_getBuffer(asset)}; + const auto font_buffer_size{static_cast(AAsset_getLength(asset))}; + + font_data_.assign( + static_cast(font_buffer), + static_cast(font_buffer) + font_buffer_size); + + AAsset_close(asset); + + if (FT_New_Memory_Face(ft_library_, font_data_.data(), font_data_.size(), 0, + &ft_face_)) { + __android_log_print(ANDROID_LOG_ERROR, "TextRenderer", + "Could not create FreeType face"); + return false; + } + + FT_Set_Pixel_Sizes(ft_face_, 0, 48); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + + if (!characters_.empty()) { + ReloadCharacters(); + } + + glBindTexture(GL_TEXTURE_2D, 0); + cardboard_demo::CHECKGLERROR("TextRenderer::LoadFont"); + + return true; +} + +void TextRenderer::RenderText(const std::string& text, + cardboard_demo::Matrix4x4 mvp, float spacing, + float x_offset, float y_offset) { + const std::u32string utf32_text {Utf8ToUtf32(text)}; + LoadGlypsForString(utf32_text); + + // Save the current GL state that will be changed. + SaveGLState(); + + glUseProgram(shader_program_); + glUniformMatrix4fv(mvp_param_, 1, GL_FALSE, mvp.ToGlArray().data()); + glUniform3f(text_color_param_, + text_color_[0], + text_color_[1], + text_color_[2]); + + glActiveTexture(GL_TEXTURE0); + + // Set GL state for text rendering. + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glDisable(GL_DEPTH_TEST); + glDisable(GL_CULL_FACE); + + glBindBuffer(GL_ARRAY_BUFFER, vbo_); + glEnableVertexAttribArray(vertex_param_); + glVertexAttribPointer(vertex_param_, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(float), + nullptr); + + float x{x_offset}; + float y{y_offset * (ft_face_->size->metrics.height >> 6)}; + for (uint32_t c : utf32_text) { + if (c == '\n') { + x = x_offset; + y -= (ft_face_->size->metrics.height >> 6); + continue; + } + + const auto it = characters_.find(c); + if (it == characters_.end()) { + __android_log_print(ANDROID_LOG_ERROR, + "TextRenderer", + "Glyph %d not found", c); + continue; + } + const Character& ch = it->second; + + const float xpos{x + ch.bearing_x}; + const float ypos{y - (ch.height - ch.bearing_y)}; + const auto w{static_cast(ch.width)}; + const auto h{static_cast(ch.height)}; + + const float vertices[6][4] = { // First triangle + {xpos, ypos + h, 0.0, 0.0}, // top left + {xpos, ypos, 0.0, 1.0}, // bottom left + {xpos + w, ypos, 1.0, 1.0}, // bottom right + + // Second triangle + {xpos, ypos + h, 0.0, 0.0}, // top right + {xpos + w, ypos, 1.0, 1.0}, // bottom right + {xpos + w, ypos + h, 1.0, 0.0}}; // top right + + glBindTexture(GL_TEXTURE_2D, ch.texture_id); + glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices); + glDrawArrays(GL_TRIANGLES, 0, 6); + + x += (ch.advance >> 6) * spacing; + } + + glDisableVertexAttribArray(vertex_param_); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindTexture(GL_TEXTURE_2D, 0); + + RestoreGLState(); +} + +void TextRenderer::SaveGLState() { + glGetIntegerv(GL_CURRENT_PROGRAM, &last_program_); + glGetBooleanv(GL_BLEND, &last_blend_enabled_); + glGetIntegerv(GL_BLEND_SRC_ALPHA, &last_blend_src_); + glGetIntegerv(GL_BLEND_DST_ALPHA, &last_blend_dst_); + glGetBooleanv(GL_CULL_FACE, &last_cull_face_enabled_); + glGetBooleanv(GL_DEPTH_TEST, &last_depth_test_enabled_); +} + +void TextRenderer::RestoreGLState() const { + glUseProgram(last_program_); + if (last_blend_enabled_) { + glEnable(GL_BLEND); + glBlendFunc(last_blend_src_, last_blend_dst_); + } else { + glDisable(GL_BLEND); + } + if (last_cull_face_enabled_) { + glEnable(GL_CULL_FACE); + } else { + glDisable(GL_CULL_FACE); + } + if (last_depth_test_enabled_) { + glEnable(GL_DEPTH_TEST); + } else { + glDisable(GL_DEPTH_TEST); + } +} + +float TextRenderer::GetTextWidth(const std::string& text, float spacing) { + float width {0.0f}; + + const std::u32string utf32_text = Utf8ToUtf32(text); + LoadGlypsForString(utf32_text); + for (uint32_t codepoint : utf32_text) { + const auto it = characters_.find(codepoint); + if (it == characters_.end()) { + __android_log_print(ANDROID_LOG_ERROR, + "TextRenderer", + "Glyph %d not found", codepoint); + continue; + } + + const Character& ch {it->second}; + width += (ch.advance >> 6) * spacing; + } + return width; +} + +void TextRenderer::SetTextColor(float r, float g, float b) { + text_color_ = {r, g, b}; +} + +void TextRenderer::LoadGlypsForString(const std::u32string& utf32_text) { + for (auto codepoint : utf32_text) { + LoadGlyph(codepoint); + } +} + +void TextRenderer::LoadGlyph(uint32_t codepoint) { + const auto it = characters_.find(codepoint); + if (it != characters_.end() && it->second.texture_id != 0 && + glIsTexture(it->second.texture_id)) { + return; + } + + GLuint old_texture_id = 0; + if (it != characters_.end()) { + old_texture_id = it->second.texture_id; + } + + // Otherwise load from FreeType + if (FT_Load_Char(ft_face_, codepoint, FT_LOAD_RENDER)) { + __android_log_print(ANDROID_LOG_ERROR, "TextRenderer", + "Failed to load glyph %d", + codepoint); + return; + } + + const GLuint texture {CreateTextureForGlyph()}; + + if (old_texture_id != 0 && glIsTexture(old_texture_id)) { + glDeleteTextures(1, &old_texture_id); + } + + characters_[codepoint] = {texture, + ft_face_->glyph->bitmap.width, + ft_face_->glyph->bitmap.rows, + ft_face_->glyph->bitmap_left, + ft_face_->glyph->bitmap_top, + ft_face_->glyph->advance.x}; +} + +void TextRenderer::ReloadCharacters() { + for (auto &[codepoint, character] : characters_) { + if (FT_Load_Char(ft_face_, codepoint, FT_LOAD_RENDER)) { + continue; + } + character.texture_id = CreateTextureForGlyph(); + } +} + +GLuint TextRenderer::CreateTextureForGlyph() const { + GLuint texture; + glGenTextures(1, &texture); + glBindTexture(GL_TEXTURE_2D, texture); + glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, ft_face_->glyph->bitmap.width, + ft_face_->glyph->bitmap.rows, 0, GL_LUMINANCE, + GL_UNSIGNED_BYTE, ft_face_->glyph->bitmap.buffer); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + return texture; +} + +} // namespace cardboard_demo diff --git a/paperscope/paperscope_launcher/src/text_renderer.h b/paperscope/paperscope_launcher/src/text_renderer.h new file mode 100644 index 00000000..70ffeb5d --- /dev/null +++ b/paperscope/paperscope_launcher/src/text_renderer.h @@ -0,0 +1,122 @@ +#ifndef PAPERSCOPE_LAUNCHER_SRC_TEXT_RENDERER_H_ +#define PAPERSCOPE_LAUNCHER_SRC_TEXT_RENDERER_H_ + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include "util.h" + +namespace cardboard_demo { + +// Holds state information for a single character glyph. +struct Character { + GLuint texture_id{}; // ID handle of the glyph texture + uint32_t width{}; + uint32_t height{}; + int32_t bearing_x{}; + int32_t bearing_y{}; + int64_t advance{}; +}; + +// Renders text using OpenGL and FreeType. +class TextRenderer { + public: + TextRenderer(); + ~TextRenderer(); + + // Initializes shaders and OpenGL buffers. Must be called on the GL thread. + void Init(); + + // Loads a font from the given asset manager. + // The font data is copied and managed by this class. + bool LoadFont(AAssetManager* asset_mgr, const std::string& font_path); + + // Renders a line of text. + void RenderText(const std::string& text, cardboard_demo::Matrix4x4 mvp, + float spacing = 1.0f, float x_offset = 0.0f, + float y_offset = 0.0f); + + /** + * Returns the width of the text in pixels, given the text and spacing. + * + * @param text The text to measure. + * @param spacing The spacing between characters. + * @return The width of the text in pixels. + */ + float GetTextWidth(const std::string& text, float spacing = 1.0f); + + /** + * Sets the text color. + * + * @param r The red component of the color from 0.0f to 1.0f. + * @param g The green component of the color from 0.0f to 1.0f. + * @param b The blue component of the color from 0.0f to 1.0f. + */ + void SetTextColor(float r, float g, float b); + + private: + // Saves the current OpenGL state. + void SaveGLState(); + // Restores the OpenGL state to its previous state. + void RestoreGLState() const; + + /** + * Loads a glyph for the given codepoint. + * + * @param codepoint The codepoint of the glyph to load. + * @return True if the glyph was loaded successfully, false otherwise. + */ + void LoadGlyph(uint32_t codepoint); + + /** + * Loads glyphs for the given string. + * + * @param utf32_text The UTF-32 string to load glyphs for. + */ + void LoadGlypsForString(const std::u32string& utf32_text); + + /** + * Reloads characters. Needed after surface is created. + * This happens when blocking the phone or when the user sends the app to the background. + */ + void ReloadCharacters(); + + /** + * Creates a texture for the given glyph. + * + * @return The texture ID of the glyph. + */ + GLuint CreateTextureForGlyph() const; + + FT_Library ft_library_; + FT_Face ft_face_; + std::vector font_data_; // To keep font data in memory. + std::unordered_map characters_; + + GLuint vbo_{}; + GLuint shader_program_{}; + GLint mvp_param_{}; + GLint text_color_param_{}; + GLint vertex_param_{}; + + GLint last_program_{}; + GLboolean last_blend_enabled_{}; + GLint last_blend_src_{}; + GLint last_blend_dst_{}; + GLboolean last_cull_face_enabled_{}; + GLboolean last_depth_test_enabled_{}; + + std::array text_color_ {1.0f, 1.0f, 1.0f}; +}; + +} // namespace cardboard_demo + +#endif // PAPERSCOPE_LAUNCHER_SRC_TEXT_RENDERER_H_ diff --git a/paperscope/paperscope_launcher/src/tutorial_animator.cc b/paperscope/paperscope_launcher/src/tutorial_animator.cc new file mode 100644 index 00000000..398fc0f5 --- /dev/null +++ b/paperscope/paperscope_launcher/src/tutorial_animator.cc @@ -0,0 +1,315 @@ +/* + * Copyright 2024 Google LLC + * + * 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. + */ +#include "tutorial_animator.h" + +#include + +#include +#include // NOLINT(build/c++11) + +#include "cardboard_strings.h" +#include "util.h" + +namespace cardboard_launcher { + +namespace { +constexpr const char* kVertexShader = R"glsl( + uniform mat4 u_MVP; + attribute vec4 a_Position; + void main () { + gl_Position = u_MVP * a_Position; + } +)glsl"; + +constexpr const char* kFragmentShader = R"glsl( + precision mediump float; + void main () { + gl_FragColor = vec4(1.0, 0.431, 0.251, 1.0); // Orange button + } +)glsl"; + +constexpr std::array kTextTutorialScale{-0.0013f, 0.0013f, 1.0f}; +constexpr std::array kTextTutorialModelMatrix{0.0f, -0.25f, 0.0f}; + +constexpr std::array kTextInstructionsModelMatrix{0.0f, -0.25f, + 0.0f}; + +constexpr std::array kNextButtonModelMatrix{0.0f, -0.5f, 0.0f}; +constexpr float kNextButtonTextXYScale{0.0012f}; +constexpr std::array kNextButtonTextScale{-kNextButtonTextXYScale, + kNextButtonTextXYScale, + 1.0f}; +} // namespace + +TutorialAnimator::TutorialAnimator( + const std::vector& button_animation_files, + const std::vector& tilt_animation_files, + cardboard_demo::TextRenderer* const text_renderer) + : button_animation_(button_animation_files), + tilt_animation_(tilt_animation_files), + text_renderer_(text_renderer) { + model_next_button_ = + cardboard_demo::GetTranslationMatrix(kNextButtonModelMatrix); +} + +void TutorialAnimator::LoadAnimations(GLuint obj_position_param, + GLuint obj_uv_param, + AAssetManager* asset_mgr, JNIEnv* env, + jobject java_asset_mgr) { + button_animation_.LoadAnimations(obj_position_param, obj_uv_param, asset_mgr, + env, java_asset_mgr); + tilt_animation_.LoadAnimations(obj_position_param, obj_uv_param, asset_mgr, + env, java_asset_mgr); + LoadNextButton(); +} + +void TutorialAnimator::SetScreenPosition( + const cardboard_demo::Matrix4x4& eye_view, + const cardboard_demo::Matrix4x4& projection_matrix) { + tilt_animation_.SetModelViewProjection(eye_view, projection_matrix); + button_animation_.SetModelViewProjection(eye_view, projection_matrix); + + // Set the model view projection of the next button. + model_view_projection_next_button_ = projection_matrix * eye_view * + button_animation_.GetModelAnimation() * + model_next_button_ * bg_scale_matrix_; + + // Set the model view projection of the next button text. + model_view_projection_next_button_text_ = + projection_matrix * eye_view * button_animation_.GetModelAnimation() * + button_text_model_matrix_ * button_text_scale_matrix_; + + // Set the model view projection of the text + const cardboard_demo::Matrix4x4 text_scale_matrix = + cardboard_demo::GetScaleMatrix(kTextTutorialScale); + const cardboard_demo::Matrix4x4 text_model_matrix = + cardboard_demo::GetTranslationMatrix(kTextTutorialModelMatrix); + model_view_projection_text_model_matrix_ = + projection_matrix * eye_view * button_animation_.GetModelAnimation() * + text_model_matrix * text_scale_matrix; + + // Set the model view projection of the tilt instructions text. + const cardboard_demo::Matrix4x4 instructions_text_model_matrix = + cardboard_demo::GetTranslationMatrix(kTextInstructionsModelMatrix); + model_view_projection_instructions_text_matrix_ = + projection_matrix * eye_view * tilt_animation_.GetModelAnimation() * + instructions_text_model_matrix * text_scale_matrix; +} + +void TutorialAnimator::DisplayTutorialText() const { + std::string press_button_string = + cardboard::GetLocalizedString(cardboard::kStrPressTheButtonOnYourViewer, + env_); + // Break the text into 2 lines. + size_t line_index {0}; + for (size_t i = press_button_string.length() / 2; + i < press_button_string.length(); + i++) { + if (press_button_string[i] == ' ') { + line_index = i; + press_button_string[i] = '\n'; + break; + } + } + + const float x_offset { + text_renderer_->GetTextWidth(press_button_string.substr(0, line_index)) * + 0.5f}; + + text_renderer_->RenderText(press_button_string, + model_view_projection_text_model_matrix_, + 1.0f, // spacing + -x_offset); +} + +void TutorialAnimator::DisplayTiltInstructionsText() const { + std::string rotate_viewer_string = + cardboard::GetLocalizedString(cardboard::kStrRotateYourViewer, + env_); + // Break the text into 2 lines. + size_t line_index {0}; + for (size_t i = rotate_viewer_string.length() / 2; + i < rotate_viewer_string.length(); + i++) { + if (rotate_viewer_string[i] == ' ') { + line_index = i; + rotate_viewer_string[i] = '\n'; + break; + } + } + + float x_offset { + text_renderer_->GetTextWidth(rotate_viewer_string.substr(0, line_index)) * + 0.5f}; + + text_renderer_->RenderText( + rotate_viewer_string, + model_view_projection_instructions_text_matrix_, 1.0f, -x_offset); + + const std::string try_it_out_now_string { + cardboard::GetLocalizedString(cardboard::kStrTryItOutNow, env_)}; + x_offset = text_renderer_->GetTextWidth( + try_it_out_now_string) * 0.5f; + + text_renderer_->RenderText( + try_it_out_now_string, + model_view_projection_instructions_text_matrix_, + 1.0f, // spacing + -x_offset, // x_offset + -3.0f); // y_offset +} + +void TutorialAnimator::DisplayButtonAnimation(GLuint program, + GLuint obj_mvp_param) { + glUseProgram(program); + const std::array target_array = + button_animation_.GetModelViewProjectionAnimation().ToGlArray(); + glUniformMatrix4fv(obj_mvp_param, 1, GL_FALSE, target_array.data()); + button_animation_.DisplayFrame(std::chrono::steady_clock::now()); + DisplayTutorialText(); + DrawNextButton(); + DrawNextButtonText(); +} + +void TutorialAnimator::DisplayTiltAnimation(GLuint program, + GLuint obj_mvp_param) { + glUseProgram(program); + const std::array target_array = + tilt_animation_.GetModelViewProjectionAnimation().ToGlArray(); + glUniformMatrix4fv(obj_mvp_param, 1, GL_FALSE, target_array.data()); + + tilt_animation_.DisplayFrame(std::chrono::steady_clock::now()); + DisplayTiltInstructionsText(); +} + +void TutorialAnimator::LoadNextButton() { + LoadNextButtonGLObjects(); + CalculateNextButtonMatrices(); +} + +void TutorialAnimator::LoadNextButtonGLObjects() { + const float vertices[]{ + 0.5f, 0.5f, 0.0f, // top right + 0.5f, -0.5f, 0.0f, // bottom right + -0.5f, -0.5f, 0.0f, // bottom left + -0.5f, 0.5f, 0.0f // top left + }; + + const uint32_t indices[]{ + // note that we start from 0! + 0, 1, 3, // first triangle + 1, 2, 3 // second triangle + }; + + glGenBuffers(1, &next_button_background_vbo_); + glGenBuffers(1, &next_button_background_ebo_); + + glBindBuffer(GL_ARRAY_BUFFER, next_button_background_vbo_); + glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); + cardboard_demo::CHECKGLERROR("LoadNextButton bind next button VBO"); + + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, next_button_background_ebo_); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, + GL_STATIC_DRAW); + + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + + const GLuint vertex_shader{ + cardboard_demo::LoadGLShader(GL_VERTEX_SHADER, kVertexShader)}; + const GLuint fragment_shader{ + cardboard_demo::LoadGLShader(GL_FRAGMENT_SHADER, kFragmentShader)}; + + next_button_background_program_id_ = glCreateProgram(); + glAttachShader(next_button_background_program_id_, vertex_shader); + glAttachShader(next_button_background_program_id_, fragment_shader); + cardboard_demo::CHECKGLERROR("LoadBlackBackground attach shaders"); + + glLinkProgram(next_button_background_program_id_); + cardboard_demo::CHECKGLERROR("LoadBlackBackground link program"); + + next_button_background_mvp_param_ = + glGetUniformLocation(next_button_background_program_id_, "u_MVP"); +} + +void TutorialAnimator::CalculateNextButtonMatrices() { + next_button_text_width_ = + text_renderer_->GetTextWidth( + cardboard::GetLocalizedString(cardboard::kStrNext, env_), 0.9f) * + kNextButtonTextXYScale; + next_button_text_x_offset_ = next_button_text_width_ / 2.0f; + + const float bg_width{1.0f * next_button_text_width_ * 1.3f}; + bg_scale_matrix_ = cardboard_demo::GetScaleMatrix({bg_width, 0.10f, 1.0f}); + + button_text_scale_matrix_ = + cardboard_demo::GetScaleMatrix(kNextButtonTextScale); + button_text_model_matrix_ = + cardboard_demo::GetTranslationMatrix({next_button_text_x_offset_, + -0.52f, // y_offset + 0.0f}); +} + +void TutorialAnimator::DrawNextButton() { + glDisable(GL_DEPTH_TEST); + glDisable(GL_CULL_FACE); + + glUseProgram(next_button_background_program_id_); + + const std::array target_array{ + model_view_projection_next_button_.ToGlArray()}; + glUniformMatrix4fv(next_button_background_mvp_param_, 1, GL_FALSE, + target_array.data()); + + glBindBuffer(GL_ARRAY_BUFFER, next_button_background_vbo_); + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), nullptr); + glEnableVertexAttribArray(0); + + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, next_button_background_ebo_); + + glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr); + + cardboard_demo::CHECKGLERROR("DrawNextButton draw elements"); + + glDisableVertexAttribArray(0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + glEnable(GL_CULL_FACE); + glEnable(GL_DEPTH_TEST); +} + +void TutorialAnimator::DrawNextButtonText() const { + text_renderer_->RenderText( + cardboard::GetLocalizedString(cardboard::kStrNext, env_), + model_view_projection_next_button_text_, + 0.9f); +} + +bool TutorialAnimator::IsNextClicked( + const cardboard_demo::Matrix4x4& device_pose, float angle_limit) const { + // Compute vectors pointing towards the reticle and towards the next button + // in head space. + cardboard_demo::Matrix4x4 head_from_demo = + device_pose * button_animation_.GetModelAnimation() * model_next_button_; + const std::array unit_quaternion {0.f, 0.f, 0.f, 1.f}; + const std::array point_vector {0.f, 0.f, -1.f, 0.f}; + const std::array target_vector = head_from_demo * unit_quaternion; + + const float angle{ + cardboard_demo::AngleBetweenVectors(point_vector, target_vector)}; + return angle < angle_limit; +} +} // namespace cardboard_launcher diff --git a/paperscope/paperscope_launcher/src/tutorial_animator.h b/paperscope/paperscope_launcher/src/tutorial_animator.h new file mode 100644 index 00000000..20073b08 --- /dev/null +++ b/paperscope/paperscope_launcher/src/tutorial_animator.h @@ -0,0 +1,163 @@ +/* + * Copyright 2024 Google LLC + * + * 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. + */ +#ifndef PAPERSCOPE_LAUNCHER_SRC_TUTORIAL_ANIMATOR_H_ +#define PAPERSCOPE_LAUNCHER_SRC_TUTORIAL_ANIMATOR_H_ + +#include +#include + +#include +#include + +#include +#include "animated_shape.h" +#include "text_renderer.h" +#include "util.h" + +namespace cardboard_launcher { + +// Handles all of the logic related to the Cardboard Demo App Tutorial. +class TutorialAnimator { + public: + /** + * Creates a TutorialAnimator from vectors of button animation frames and + * tilt animation frames. + * + * @param button_animation_files A vector of button animation frames. + * @param tilt_animation_files A vector of tilt animation frames. + * @param device_pose pointer to the device pose. Used to determine when the + * device is in Portrait orientation. + */ + TutorialAnimator(const std::vector& button_animation_files, + const std::vector& tilt_animation_files, + cardboard_demo::TextRenderer* const text_renderer); + + /** + * Loads the animation files for both the button and tilt animations. + * + * @param obj_position_param The object position parameter. + * @param obj_uv_param The object uv parameter. + * @param asset_mgr The asset manager. + * @param env The JNI environment. + * @param java_asset_mgr The Java asset manager. + */ + void LoadAnimations(GLuint obj_position_param, GLuint obj_uv_param, + AAssetManager* asset_mgr, JNIEnv* env, + jobject java_asset_mgr); + + /** + * Sets the position of the animation on screen. + * + * @param eye_view The eye view matrix. + * @param projection_matrix The projection matrix. + */ + void SetScreenPosition(const cardboard_demo::Matrix4x4& eye_view, + const cardboard_demo::Matrix4x4& projection_matrix); + + /** + * Draw the current frame of the animation to be drawn on screen. + * + * @param program The OpenGL program. + * @param obj_mvp_param The OpenGL MVP matrix. + */ + void DisplayButtonAnimation(GLuint program, GLuint obj_mvp_param); + + /** + * Draw the current frame of the animation to be drawn on screen. + * + * @param program The OpenGL program. + * @param obj_mvp_param The OpenGL MVP matrix. + */ + void DisplayTiltAnimation(GLuint program, GLuint obj_mvp_param); + + /** + * Displays the tutorial text on screen. + */ + void DisplayTutorialText() const; + + /** + * Displays the tilt instructions on screen. + */ + void DisplayTiltInstructionsText() const; + + /** + * Loads the assets for the "Next" button. + */ + void LoadNextButton(); + + /** + * Loads the OpenGL objects for the "Next" button background. + */ + void LoadNextButtonGLObjects(); + + /** + * Calculates the matrices needed for the "Next" button background + * and text to be drawn on screen. + */ + void CalculateNextButtonMatrices(); + + /** + * Draws the "Next" button background. + */ + void DrawNextButton(); + + /** + * Draws the text for the "Next" button. + */ + void DrawNextButtonText() const; + + /** + * Indicates whether the Next button has been clicked. + * @param device_pose The device pose. + * @param angle_limit Angle threshold for determining whether the controller + * is pointing at the next button. + */ + bool IsNextClicked(const cardboard_demo::Matrix4x4& device_pose, + float angle_limit) const; + + /** + * Sets the JNI environment. + * @param env The JNI environment. + */ + void SetJNIEnv(JNIEnv* env) { env_ = env; } + + private: + AnimatedShape button_animation_; + AnimatedShape tilt_animation_; + cardboard_demo::TextRenderer* const text_renderer_; + JNIEnv* env_; + + cardboard_demo::Matrix4x4 model_next_button_; + cardboard_demo::Matrix4x4 model_view_projection_next_button_; + cardboard_demo::Matrix4x4 model_view_projection_next_button_text_; + cardboard_demo::Matrix4x4 model_view_projection_text_model_matrix_; + cardboard_demo::Matrix4x4 model_view_projection_instructions_text_matrix_; + + cardboard_demo::Matrix4x4 bg_scale_matrix_; + + float next_button_text_width_{}; + float next_button_text_x_offset_{}; + cardboard_demo::Matrix4x4 button_text_scale_matrix_; + cardboard_demo::Matrix4x4 button_text_model_matrix_; + + GLuint next_button_background_vbo_{}; + GLuint next_button_background_ebo_{}; + GLuint next_button_background_program_id_{}; + GLuint next_button_background_mvp_param_{}; +}; + +} // namespace cardboard_launcher +#endif // PAPERSCOPE_LAUNCHER_SRC_TUTORIAL_ANIMATOR_H_ diff --git a/paperscope/paperscope_launcher/src/util.cc b/paperscope/paperscope_launcher/src/util.cc new file mode 100644 index 00000000..18d92ade --- /dev/null +++ b/paperscope/paperscope_launcher/src/util.cc @@ -0,0 +1,578 @@ +/* + * Copyright 2019 Google LLC + * + * 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. + */ +#include "util.h" + +#include +#include // Needed for strtok_r and strstr +#include +#include + +#include +#include +#include +#include +#include + +#include + +namespace cardboard_demo { + +namespace { + +class RunAtEndOfScope { + public: + RunAtEndOfScope(std::function function) : function_(function) {} + + ~RunAtEndOfScope() { function_(); } + + private: + std::function function_; +}; + +/** + * Loads a png file from assets folder and then assigns it to the OpenGL target. + * This method must be called from the renderer thread since it will result in + * OpenGL calls to assign the image to the texture target. + * + * @param env The JNIEnv to use. + * @param java_asset_mgr The asset manager object. + * @param target OpenGL texture target to load the image into. + * @param path Path to the file, relative to the assets folder. + * @return true if png is loaded correctly, otherwise false. + */ +bool LoadPngFromAssetManager(JNIEnv* env, jobject java_asset_mgr, int target, + const std::string& path) { + jclass bitmap_factory_class = + env->FindClass("android/graphics/BitmapFactory"); + jclass asset_manager_class = + env->FindClass("android/content/res/AssetManager"); + jclass gl_utils_class = env->FindClass("android/opengl/GLUtils"); + jmethodID decode_stream_method = env->GetStaticMethodID( + bitmap_factory_class, "decodeStream", + "(Ljava/io/InputStream;)Landroid/graphics/Bitmap;"); + jmethodID open_method = env->GetMethodID( + asset_manager_class, "open", "(Ljava/lang/String;)Ljava/io/InputStream;"); + jmethodID tex_image_2d_method = env->GetStaticMethodID( + gl_utils_class, "texImage2D", "(IILandroid/graphics/Bitmap;I)V"); + + jstring j_path = env->NewStringUTF(path.c_str()); + RunAtEndOfScope cleanup_j_path([&] { + if (j_path) { + env->DeleteLocalRef(j_path); + } + }); + + jobject image_stream = + env->CallObjectMethod(java_asset_mgr, open_method, j_path); + jobject image_obj = env->CallStaticObjectMethod( + bitmap_factory_class, decode_stream_method, image_stream); + if (env->ExceptionOccurred() != nullptr) { + LOGE("Java exception while loading image"); + env->ExceptionClear(); + image_obj = nullptr; + return false; + } + + env->CallStaticVoidMethod(gl_utils_class, tex_image_2d_method, target, 0, + image_obj, 0); + return true; +} + +/** + * Loads obj file from assets folder from the app. + * + * This sample uses the .obj format since .obj is straightforward to parse and + * the sample is intended to be self-contained, but a real application + * should probably use a library to load a more modern format, such as FBX or + * glTF. + * + * @param mgr AAssetManager pointer. + * @param file_name Name of the obj file. + * @param out_vertices Output vertices. + * @param out_normals Output normals. + * @param out_uv Output texture UV coordinates. + * @param out_indices Output triangle indices. + * @return true if obj is loaded correctly, otherwise false. + */ +bool LoadObjFile(AAssetManager* mgr, const std::string& file_name, + std::vector* out_vertices, + std::vector* out_normals, + std::vector* out_uv, + std::vector* out_indices) { + std::vector temp_positions; + std::vector temp_normals; + std::vector temp_uvs; + std::vector vertex_indices; + std::vector normal_indices; + std::vector uv_indices; + + // If the file hasn't been uncompressed, load it to the internal storage. + // Note that AAsset_openFileDescriptor doesn't support compressed + // files (.obj). + AAsset* asset = + AAssetManager_open(mgr, file_name.c_str(), AASSET_MODE_STREAMING); + if (asset == nullptr) { + LOGE("Error opening asset %s", file_name.c_str()); + return false; + } + + off_t file_size = AAsset_getLength(asset); + std::string file_buffer; + file_buffer.resize(file_size); + int ret = AAsset_read(asset, &file_buffer.front(), file_size); + AAsset_close(asset); + + if (ret < 0 || ret == EOF) { + LOGE("Failed to open file: %s", file_name.c_str()); + return false; + } + + std::stringstream file_string_stream(file_buffer); + + while (file_string_stream && !file_string_stream.eof()) { + char line_header[128]; + file_string_stream.getline(line_header, 128); + + if (line_header[0] == '\0') { + continue; + } else if (line_header[0] == 'v' && line_header[1] == 'n') { + // Parse vertex normal. + GLfloat normal[3]; + int matches = sscanf(line_header, "vn %f %f %f\n", &normal[0], &normal[1], + &normal[2]); + if (matches != 3) { + LOGE("Format of 'vn float float float' required for each normal line"); + return false; + } + + temp_normals.push_back(normal[0]); + temp_normals.push_back(normal[1]); + temp_normals.push_back(normal[2]); + } else if (line_header[0] == 'v' && line_header[1] == 't') { + // Parse texture uv. + GLfloat uv[2]; + int matches = sscanf(line_header, "vt %f %f\n", &uv[0], &uv[1]); + if (matches != 2) { + LOGE("Format of 'vt float float' required for each texture uv line"); + return false; + } + + temp_uvs.push_back(uv[0]); + temp_uvs.push_back(uv[1]); + } else if (line_header[0] == 'v') { + // Parse vertex. + GLfloat vertex[3]; + int matches = sscanf(line_header, "v %f %f %f\n", &vertex[0], &vertex[1], + &vertex[2]); + if (matches != 3) { + LOGE("Format of 'v float float float' required for each vertex line"); + return false; + } + + temp_positions.push_back(vertex[0]); + temp_positions.push_back(vertex[1]); + temp_positions.push_back(vertex[2]); + } else if (line_header[0] == 'f') { + // Actual faces information starts from the second character. + char* face_line = &line_header[1]; + + unsigned int vertex_index[4]; + unsigned int normal_index[4]; + unsigned int texture_index[4]; + + std::vector per_vertex_info_list; + char* per_vertex_info_list_c_str; + char* face_line_iter = face_line; + while ((per_vertex_info_list_c_str = + strtok_r(face_line_iter, " ", &face_line_iter))) { + // Divide each faces information into individual positions. + per_vertex_info_list.push_back(per_vertex_info_list_c_str); + } + + bool is_normal_available = false; + bool is_uv_available = false; + for (size_t i = 0; i < per_vertex_info_list.size(); ++i) { + char* per_vertex_info; + int per_vertex_info_count = 0; + + const bool is_vertex_normal_only_face = + strstr(per_vertex_info_list[i], "//") != nullptr; + + char* per_vertex_info_iter = per_vertex_info_list[i]; + while ((per_vertex_info = strtok_r(per_vertex_info_iter, "/", + &per_vertex_info_iter))) { + // write only normal and vert values. + switch (per_vertex_info_count) { + case 0: + // Write to vertex indices. + vertex_index[i] = atoi(per_vertex_info); // NOLINT + break; + case 1: + // Write to texture indices. + if (is_vertex_normal_only_face) { + normal_index[i] = atoi(per_vertex_info); // NOLINT + is_normal_available = true; + } else { + texture_index[i] = atoi(per_vertex_info); // NOLINT + is_uv_available = true; + } + break; + case 2: + // Write to normal indices. + if (!is_vertex_normal_only_face) { + normal_index[i] = atoi(per_vertex_info); // NOLINT + is_normal_available = true; + break; + } + [[clang::fallthrough]]; + // Fallthrough to error case because if there's no texture coords, + // there should only be 2 indices per vertex (position and + // normal). + default: + // Error formatting. + LOGE( + "Format of 'f int/int/int int/int/int int/int/int " + "(int/int/int)' " + "or 'f int//int int//int int//int (int//int)' required for " + "each face"); + return false; + } + per_vertex_info_count++; + } + } + + const int vertices_count = per_vertex_info_list.size(); + for (int i = 2; i < vertices_count; ++i) { + vertex_indices.push_back(vertex_index[0] - 1); + vertex_indices.push_back(vertex_index[i - 1] - 1); + vertex_indices.push_back(vertex_index[i] - 1); + + if (is_normal_available) { + normal_indices.push_back(normal_index[0] - 1); + normal_indices.push_back(normal_index[i - 1] - 1); + normal_indices.push_back(normal_index[i] - 1); + } + + if (is_uv_available) { + uv_indices.push_back(texture_index[0] - 1); + uv_indices.push_back(texture_index[i - 1] - 1); + uv_indices.push_back(texture_index[i] - 1); + } + } + } + } + + const bool is_normal_available = !normal_indices.empty(); + const bool is_uv_available = !uv_indices.empty(); + + if (is_normal_available && normal_indices.size() != vertex_indices.size()) { + LOGE("Obj normal indices does not equal to vertex indices."); + return false; + } + + if (is_uv_available && uv_indices.size() != vertex_indices.size()) { + LOGE("Obj UV indices does not equal to vertex indices."); + return false; + } + + for (unsigned int i = 0; i < vertex_indices.size(); i++) { + unsigned int vertex_index = vertex_indices[i]; + out_vertices->push_back(temp_positions[vertex_index * 3]); + out_vertices->push_back(temp_positions[vertex_index * 3 + 1]); + out_vertices->push_back(temp_positions[vertex_index * 3 + 2]); + out_indices->push_back(i); + + if (is_normal_available) { + unsigned int normal_index = normal_indices[i]; + out_normals->push_back(temp_normals[normal_index * 3]); + out_normals->push_back(temp_normals[normal_index * 3 + 1]); + out_normals->push_back(temp_normals[normal_index * 3 + 2]); + } + + if (is_uv_available) { + unsigned int uv_index = uv_indices[i]; + out_uv->push_back(temp_uvs[uv_index * 2]); + out_uv->push_back(temp_uvs[uv_index * 2 + 1]); + } + } + + return true; +} + +/** + * Calculates vector norm + * + * @param vec Vector + * @return Norm value + */ +float VectorNorm(const std::array& vec) { + return std::sqrt(vec[0] * vec[0] + vec[1] * vec[1] + vec[2] * vec[2]); +} + +/** + * Calculates dot product of two vectors + * + * @param vec1 First vector + * @param vec2 Second vector + * @return Dot product value. + */ +float VectorDotProduct(const std::array& vec1, + const std::array& vec2) { + float product = 0; + for (int i = 0; i < 3; i++) { + product += vec1[i] * vec2[i]; + } + return product; +} + +} // anonymous namespace + +Matrix4x4 Matrix4x4::operator*(const Matrix4x4& right) const { + Matrix4x4 result; + for (int i = 0; i < 4; ++i) { + for (int j = 0; j < 4; ++j) { + result.m[i][j] = 0.0f; + for (int k = 0; k < 4; ++k) { + result.m[i][j] += this->m[k][j] * right.m[i][k]; + } + } + } + return result; +} + +std::array Matrix4x4::operator*(const std::array& vec) { + std::array result; + for (int i = 0; i < 4; ++i) { + result[i] = 0; + for (int k = 0; k < 4; ++k) { + result[i] += this->m[k][i] * vec[k]; + } + } + return result; +} + +std::array Matrix4x4::ToGlArray() { + std::array result; + memcpy(&result[0], m, 16 * sizeof(float)); + return result; +} + +Matrix4x4 Quatf::ToMatrix() { + // Based on ion::math::RotationMatrix3x3 + const float xx = 2 * x * x; + const float yy = 2 * y * y; + const float zz = 2 * z * z; + + const float xy = 2 * x * y; + const float xz = 2 * x * z; + const float yz = 2 * y * z; + + const float xw = 2 * x * w; + const float yw = 2 * y * w; + const float zw = 2 * z * w; + + Matrix4x4 m; + m.m[0][0] = 1 - yy - zz; + m.m[0][1] = xy + zw; + m.m[0][2] = xz - yw; + m.m[0][3] = 0; + m.m[1][0] = xy - zw; + m.m[1][1] = 1 - xx - zz; + m.m[1][2] = yz + xw; + m.m[1][3] = 0; + m.m[2][0] = xz + yw; + m.m[2][1] = yz - xw; + m.m[2][2] = 1 - xx - yy; + m.m[2][3] = 0; + m.m[3][0] = 0; + m.m[3][1] = 0; + m.m[3][2] = 0; + m.m[3][3] = 1; + + return m; +} + +Matrix4x4 GetMatrixFromGlArray(float* vec) { + Matrix4x4 result; + memcpy(result.m, vec, 16 * sizeof(float)); + return result; +} + +Matrix4x4 GetScaleMatrix(const std::array& scale){ + return {{{scale.at(0), 0.0f, 0.0f, 0.0f}, + {0.0f, scale.at(1), 0.0f, 0.0f}, + {0.0f, 0.0f, scale.at(2), 0.0f}, + {0.0f, 0.0f, 0.0f, 1.0f}}}; +} + +Matrix4x4 GetTranslationMatrix(const std::array& translation) { + return {{{1.0f, 0.0f, 0.0f, 0.0f}, + {0.0f, 1.0f, 0.0f, 0.0f}, + {0.0f, 0.0f, 1.0f, 0.0f}, + {translation.at(0), translation.at(1), translation.at(2), 1.0f}}}; +} + +Matrix4x4 GetYawRotationMatrix(const float yaw_degrees){ + return {{{cos(yaw_degrees), 0.0f, -sin(yaw_degrees), 0.0f}, + {0.0f, 1.0f, 0.0f, 0.0f}, + {sin(yaw_degrees), 0.0f, cos(yaw_degrees), 0.0f}, + {0.0f, 0.0f, 0.0f, 1.0f}}}; +} + +Matrix4x4 GetPitchRotationMatrix(const float pitch_degrees){ + return {{{1.0f, 0.0f, 0.0f, 0.0f}, + {0.0f, cos(pitch_degrees), sin(pitch_degrees), 0.0f}, + {0.0f, -sin(pitch_degrees), cos(pitch_degrees), 0.0f}, + {0.0f, 0.0f, 0.0f, 1.0f}}}; +} + +Matrix4x4 GetRollRotationMatrix(const float roll_degrees){ + return {{{cos(roll_degrees), sin(roll_degrees), 0.0f, 0.0f}, + {-sin(roll_degrees), cos(roll_degrees), 0.0f, 0.0f}, + {0.0f, 0.0f, 1.0f, 0.0f}, + {0.0f, 0.0f, 0.0f, 1.0f}}}; +} + +float AngleBetweenVectors(const std::array& vec1, + const std::array& vec2) { + return std::acos( + std::max(-1.f, std::min(1.f, VectorDotProduct(vec1, vec2) / + (VectorNorm(vec1) * VectorNorm(vec2))))); +} + +static constexpr uint64_t kNanosInSeconds = 1000000000; + +int64_t GetBootTimeNano() { + struct timespec res; + clock_gettime(CLOCK_BOOTTIME, &res); + return (res.tv_sec * kNanosInSeconds) + res.tv_nsec; +} + +float RandomUniformFloat(float min, float max) { + static std::random_device random_device; + static std::mt19937 random_generator(random_device()); + static std::uniform_real_distribution random_distribution(0, 1); + return random_distribution(random_generator) * (max - min) + min; +} + +int RandomUniformInt(int max_val) { + static std::random_device random_device; + static std::mt19937 random_generator(random_device()); + std::uniform_int_distribution random_distribution(0, max_val - 1); + return random_distribution(random_generator); +} + +void CheckGlError(const char* file, int line, const char* label) { + int gl_error = glGetError(); + if (gl_error != GL_NO_ERROR) { + LOGE("%s : %d > GL error @ %s: %d", file, line, label, gl_error); + // Crash immediately to make OpenGL errors obvious. + abort(); + } +} + +GLuint LoadGLShader(GLenum type, const char* shader_source) { + GLuint shader = glCreateShader(type); + glShaderSource(shader, 1, &shader_source, nullptr); + glCompileShader(shader); + + // Get the compilation status. + GLint compile_status; + glGetShaderiv(shader, GL_COMPILE_STATUS, &compile_status); + + // If the compilation failed, delete the shader and show an error. + if (compile_status == 0) { + GLint info_len = 0; + glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &info_len); + if (info_len == 0) { + return 0; + } + + std::vector info_string(info_len); + glGetShaderInfoLog(shader, info_string.size(), nullptr, info_string.data()); + LOGE("Could not compile shader of type %d: %s", type, info_string.data()); + glDeleteShader(shader); + return 0; + } else { + return shader; + } +} + +bool TexturedMesh::Initialize(GLuint position_attrib, GLuint uv_attrib, + const std::string& obj_file_path, + AAssetManager* asset_mgr) { + ClearMesh(); + position_attrib_ = position_attrib; + uv_attrib_ = uv_attrib; + // We don't use normals for anything so we discard them. + std::vector normals; + if (!LoadObjFile(asset_mgr, obj_file_path, &vertices_, &normals, &uv_, + &indices_)) { + return false; + } + return true; +} + +void TexturedMesh::ClearMesh() { + position_attrib_ = 0; + uv_attrib_ = 0; + vertices_.clear(); + uv_.clear(); + indices_.clear(); +} + +void TexturedMesh::Draw() const { + glEnableVertexAttribArray(position_attrib_); + glVertexAttribPointer(position_attrib_, 3, GL_FLOAT, false, 0, + vertices_.data()); + glEnableVertexAttribArray(uv_attrib_); + glVertexAttribPointer(uv_attrib_, 2, GL_FLOAT, false, 0, uv_.data()); + + glDrawElements(GL_TRIANGLES, indices_.size(), GL_UNSIGNED_SHORT, + indices_.data()); +} + +Texture::~Texture() { + if (texture_id_ != 0) { + glDeleteTextures(1, &texture_id_); + } +} + +bool Texture::Initialize(JNIEnv* env, jobject java_asset_mgr, + const std::string& texture_path) { + glGenTextures(1, &texture_id_); + Bind(); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, + GL_LINEAR_MIPMAP_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + if (!LoadPngFromAssetManager(env, java_asset_mgr, GL_TEXTURE_2D, + texture_path)) { + LOGE("Couldn't load texture."); + return false; + } + glGenerateMipmap(GL_TEXTURE_2D); + return true; +} + +void Texture::Bind() const { + CARDBOARDDEMO_CHECK(texture_id_ != 0); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, texture_id_); +} + +} // namespace cardboard_demo diff --git a/paperscope/paperscope_launcher/src/util.h b/paperscope/paperscope_launcher/src/util.h new file mode 100644 index 00000000..ec703123 --- /dev/null +++ b/paperscope/paperscope_launcher/src/util.h @@ -0,0 +1,241 @@ +/* + * Copyright 2024 Google LLC + * + * 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. + */ +#ifndef PAPERSCOPE_LAUNCHER_SRC_UTIL_H_ +#define PAPERSCOPE_LAUNCHER_SRC_UTIL_H_ + +#include +#include + +#include +#include + +#include + +#define LOG_TAG "HelloCardboardApp" +#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) +#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) +#define CARDBOARDDEMO_CHECK(condition) \ + if (!(condition)) { \ + LOGE("*** CHECK FAILED at %s:%d: %s", __FILE__, __LINE__, #condition); \ + abort(); \ + } + +namespace cardboard_demo { +/* + * A 4x4 matrix used to represent pose of objects in world space. + * + * The Coordinate system is right handed. + * + * When holding the device in portrait in front of the user and looking at the + * screen: + * + * X points to the right, + * Y points up, + * Z points towards the user, i.e. With reference to the user, +Z points + * backwards and -Z points forwards. + * + * Yaw is the clockwise rotation around the Y axis, + * Pitch is the clockwise rotation around the X axis, + * Roll is the clockwise rotation around the Z axis. + */ +class Matrix4x4 { + public: + float m[4][4]; + + // Multiplies two matrices. + Matrix4x4 operator*(const Matrix4x4& right) const; + + // Multiplies a matrix with a vector. + std::array operator*(const std::array& vec); + + // Converts a matrix to an array of floats suitable for passing to OpenGL. + std::array ToGlArray(); +}; + +struct Quatf { + float x; + float y; + float z; + float w; + + Quatf(float x_, float y_, float z_, float w_) : x(x_), y(y_), z(z_), w(w_) {} + + Quatf() : x(0), y(0), z(0), w(1) {} + + static Quatf FromXYZW(float q[4]) { return Quatf(q[0], q[1], q[2], q[3]); } + + Matrix4x4 ToMatrix(); +}; + +/** + * Converts an array of floats to a matrix. + * + * @param vec GL array + * @return Obtained matrix + */ +Matrix4x4 GetMatrixFromGlArray(float* vec); + +/** + * Construct a scale matrix. + * + * @param scale Scale array + * @return Obtained matrix + */ +Matrix4x4 GetScaleMatrix(const std::array& scale); + +/** + * Construct a translation matrix. + * + * @param translation Translation array + * @return Obtained matrix + */ +Matrix4x4 GetTranslationMatrix(const std::array& translation); + +/** + * Construct a yaw rotation matrix (Rotation around the y axis). + * + * @param yaw Yaw angle in degrees. + * @return Obtained matrix + */ +Matrix4x4 GetYawRotationMatrix(const float yaw_degrees); + +/** + * Construct a pitch rotation matrix (Rotation around the x axis). + * + * @param pitch Pitch angle in degrees. + * @return Obtained matrix + */ +Matrix4x4 GetPitchRotationMatrix(const float pitch_degrees); + +/** + * Construct a roll rotation matrix (Rotation around the z axis). + * + * @param roll Roll angle in degrees. + * @return Obtained matrix + */ +Matrix4x4 GetRollRotationMatrix(const float roll_degrees); + +/** + * Computes the angle between two vectors. + * + * @param vec1 First vector + * @param vec2 Second vector + * @return Angle between the vectors + */ +float AngleBetweenVectors(const std::array& vec1, + const std::array& vec2); + +/** + * Gets system boot time in nanoseconds. + * + * @return System boot time in nanoseconds + */ +int64_t GetBootTimeNano(); + +/** + * Generates a random floating point number between |min| and |max|. + * + * @param min Minimum range + * @param max Maximum range + * @return Random float number + */ +float RandomUniformFloat(float min, float max); + +/** + * Generates a random integer in the range [0, max_val). + * + * @param max_val Maximum range + * @return Random int number + */ +int RandomUniformInt(int max_val); + +/** + * Checks for OpenGL errors, and crashes if one has occurred. Note that this + * can be an expensive call, so real applications should call this rarely. + * + * @param file File name + * @param line Line number + * @param label Error label + */ +void CheckGlError(const char* file, int line, const char* label); + +#define CHECKGLERROR(label) CheckGlError(__FILE__, __LINE__, label) + +/** + * Converts a string into an OpenGL ES shader. + * + * @param type The type of shader (GL_VERTEX_SHADER or GL_FRAGMENT_SHADER). + * @param shader_source The source code of the shader. + * @return The shader object handler, or 0 if there's an error. + */ +GLuint LoadGLShader(GLenum type, const char* shader_source); + +class TexturedMesh { + public: + TexturedMesh() = default; + + // Initializes the mesh from a .obj file. + // + // @return True if initialization was successful. + bool Initialize(GLuint position_attrib, GLuint uv_attrib, + const std::string& obj_file_path, AAssetManager* asset_mgr); + + // Draws the mesh. The u_MVP uniform should be set before calling this using + // glUniformMatrix4fv(), and a texture should be bound to GL_TEXTURE0. + void Draw() const; + + private: + // Clears all data related to current Mesh. + void ClearMesh(); + + std::vector vertices_; + std::vector uv_; + std::vector indices_; + GLuint position_attrib_{0}; + GLuint uv_attrib_{0}; +}; + +class Texture { + public: + Texture() = default; + + ~Texture(); + + // Initializes the texture. + // + // After this is called the texture will be bound, replacing any previously + // bound texture. + // + // @return True if initialization was successful. + // TODO(b/138789810): Share some parts of the code between Android and iOS + // samples. + bool Initialize(JNIEnv* env, jobject java_asset_mgr, + const std::string& texture_path); + + // Binds the texture, replacing any previously bound texture. + void Bind() const; + + GLuint get_texture_id() const { return texture_id_; } + void set_texture_id(GLuint texture_id) { texture_id_ = texture_id; } + + private: + GLuint texture_id_{0}; +}; + +} // namespace cardboard_demo + +#endif // PAPERSCOPE_LAUNCHER_SRC_UTIL_H_ diff --git a/paperscope/photosphere/AndroidManifest.xml b/paperscope/photosphere/AndroidManifest.xml new file mode 100644 index 00000000..fe9e1f40 --- /dev/null +++ b/paperscope/photosphere/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + diff --git a/paperscope/photosphere/PhotoSphereActivity.java b/paperscope/photosphere/PhotoSphereActivity.java new file mode 100644 index 00000000..09ed2efd --- /dev/null +++ b/paperscope/photosphere/PhotoSphereActivity.java @@ -0,0 +1,19 @@ +package com.google.vr.cardboard.paperscope.demo.photosphere; + +import com.google.vr.cardboard.paperscope.demo.common.DemoBaseActivity; +import com.google.vr.cardboard.paperscope.demo.common.DemoBaseController; + +/** + * Demo that allows the user to view Photo Spheres stored on their device. Magnet pull toggles + * through the Photo Spheres. + */ +public class PhotoSphereActivity extends DemoBaseActivity { + private PhotoSphereController photoSphereController; + + @Override + protected DemoBaseController createController() { + photoSphereController = new PhotoSphereController(cardboardView); + photoSphereController.createPhotoSphere(); + return photoSphereController; + } +} diff --git a/paperscope/photosphere/PhotoSphereController.java b/paperscope/photosphere/PhotoSphereController.java new file mode 100644 index 00000000..acec0652 --- /dev/null +++ b/paperscope/photosphere/PhotoSphereController.java @@ -0,0 +1,198 @@ +package com.google.vr.cardboard.paperscope.demo.photosphere; + +import android.database.Cursor; +import android.net.Uri; +import android.provider.MediaStore.Images.Media; +import android.provider.MediaStore.MediaColumns; +import com.google.cardboard.sdk.CardboardView; +import com.google.common.flogger.FluentLogger; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.vr.cardboard.paperscope.demo.common.DemoBaseController; +import com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.util.PanoMetadata; +import com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.viewer.PanoramaImage; +import com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.viewer.TileProvider; +import com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.viewer.TileProviderImpl; +import java.io.IOException; +import java.io.InputStream; +import org.jspecify.annotations.Nullable; + +/** Demo that allows the user to view Photo Spheres. */ +public class PhotoSphereController extends DemoBaseController { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + private boolean hasValidPhotoSphere; + private Uri contentUri; + private Cursor cursor; + private static final String NO_PHOTO_SPHERE_FOUND_ASSET = "photosphere/no_photo_sphere_found.jpg"; + + public PhotoSphereController(CardboardView cardboardView) { + super(cardboardView); + if (cardboardView == null) { + throw new IllegalArgumentException("CardboardView is null"); + } + this.renderer = new PhotoSphereRenderer(cardboardView.getContext()); + this.cardboardView.setRenderer(renderer); + } + + @Override + public void enableOnTriggerEventListener() { + cardboardView.setOnTriggerEvent( + () -> { + logger.atInfo().log("Trigger event received, switching photo."); + if (renderer != null && hasValidPhotoSphere) { + nextPhotoSphere(); + } + }); + } + + public void createPhotoSphere() { + logger.atInfo().log("createPhotoSphere"); + setUpCursor(); + hasValidPhotoSphere = nextPhotoSphere(); + + if (!hasValidPhotoSphere) { + loadDefaultPhotoSphere(); + } + } + + private void setUpCursor() { + String[] projection = {Media._ID, MediaColumns.DISPLAY_NAME}; + + String selection = MediaColumns.DISPLAY_NAME + " LIKE 'PANO%'"; + cursor = + cardboardView + .getContext() + .getContentResolver() + .query( + Media.EXTERNAL_CONTENT_URI, + projection, + selection, + null, + MediaColumns.DATE_ADDED + " DESC"); + } + + private void loadDefaultPhotoSphere() { + if (renderer == null) { + logger.atWarning().log( + "Renderer must be initialized before loading the default photo sphere."); + return; + } + + PanoramaImage defaultPano = createPanoramaImage(NO_PHOTO_SPHERE_FOUND_ASSET); + if (defaultPano != null) { + renderer.setPanoramaImage(defaultPano); + } else { + logger.atWarning().log("Unable to create PanoramaImage from asset!"); + } + } + + private @Nullable PanoramaImage createPanoramaImage(String assetPath) { + TileProvider tileProvider = createTileProvider(assetPath); + if (tileProvider == null) { + return null; + } + + PanoMetadata metadata = createPanoMetadata(assetPath); + if (metadata == null) { + return null; + } + // Reset the photo heading so that it's shown centered. + metadata.setPoseHeadingDegrees(0); + logger.atInfo().log("Metadata: %s", metadata); + return new PanoramaImage(tileProvider, metadata); + } + + private @Nullable PanoramaImage createPanoramaImage(Uri uri) { + logger.atInfo().log("createPanoramaImage for uri: %s", uri); + TileProvider tileProvider = createTileProvider(uri); + PanoMetadata metadata = createPanoMetadata(uri); + // Reset the photo heading so that it's shown centered. + if (metadata != null) { + metadata.setPoseHeadingDegrees(0); + logger.atFine().log("Metadata: %s", metadata); + if (tileProvider != null) { + return new PanoramaImage(tileProvider, metadata); + } + } + + return null; + } + + private @Nullable TileProvider createTileProvider(Uri uri) { + try (InputStream in = cardboardView.getContext().getContentResolver().openInputStream(uri)) { + return new TileProviderImpl(in); + } catch (IOException | RuntimeException e) { + logger.atWarning().withCause(e).log("Failed to create tile provider for uri: %s", uri); + return null; + } + } + + private @Nullable TileProvider createTileProvider(String assetPath) { + try (InputStream in = cardboardView.getContext().getAssets().open(assetPath)) { + return new TileProviderImpl(in); + } catch (IOException e) { + logger.atWarning().withCause(e).log( + "Failed to create tile provider for asset: %s", assetPath); + return null; + } + } + + private PanoMetadata createPanoMetadata(String assetPath) { + return PanoMetadata.parse( + () -> { + try { + return cardboardView.getContext().getAssets().open(assetPath); + } catch (IOException e) { + logger.atWarning().withCause(e).log("Failed to open asset file: %s", assetPath); + return null; + } + }); + } + + private PanoMetadata createPanoMetadata(Uri uri) { + return PanoMetadata.parse( + () -> { + try { + return cardboardView.getContext().getContentResolver().openInputStream(uri); + } catch (IOException e) { + logger.atWarning().withCause(e).log("Failed to open uri: %s", uri); + return null; + } + }); + } + + @CanIgnoreReturnValue + private boolean nextPhotoSphere() { + boolean foundValidPhotoSphere = false; + if (cursor != null && cursor.getCount() > 0) { + boolean done = false; + boolean cycled = false; + while (!done) { + if (!cursor.moveToNext()) { + cursor.moveToFirst(); // Cycle back to beginning. + if (cycled) { + // Set done to true here so the while loop doesn't loop infinitely + // in the case where there are photos in the cursor but + // createPanoramaImage() fails on every photo. + done = true; + } + cycled = true; + } + contentUri = Uri.parse(Media.EXTERNAL_CONTENT_URI + "/" + cursor.getString(0)); + logger.atInfo().log("Switching to pano %s at uri: %s", cursor.getString(1), contentUri); + + PanoramaImage pano = createPanoramaImage(contentUri); + if (pano != null) { + renderer.setPanoramaImage(pano); + done = true; + foundValidPhotoSphere = true; + } else { + logger.atWarning().log("Unable to create PanoramaImage!"); + } + } + } else { + logger.atInfo().log("Cursor is empty"); + } + + return foundValidPhotoSphere; + } +} diff --git a/paperscope/photosphere/PhotoSphereRenderer.java b/paperscope/photosphere/PhotoSphereRenderer.java new file mode 100644 index 00000000..2551ff84 --- /dev/null +++ b/paperscope/photosphere/PhotoSphereRenderer.java @@ -0,0 +1,98 @@ +package com.google.vr.cardboard.paperscope.demo.photosphere; + +import android.content.Context; +import com.google.cardboard.sdk.CardboardView; +import com.google.cardboard.sdk.HeadTransform; +import com.google.cardboard.sdk.Viewport; +import com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.viewer.PanoramaImage; +import com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.viewer.PanoramaViewRenderer; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +/** + * A wrapper around a PanoramaViewRenderer that implements CardboardView.Renderer so that we can use + * it in a CardboardView. + */ +final class PhotoSphereRenderer implements CardboardView.Renderer { + private static final String TAG = PhotoSphereRenderer.class.getSimpleName(); + private static final float Z_NEAR = 0.1f; + private static final float Z_FAR = 100.0f; + + private PanoramaViewRenderer renderer; + private Context context; + private final float[] headViewMatrix = new float[16]; + + public PhotoSphereRenderer(Context context) { + this.context = context; + } + + public void setPanoramaImage(PanoramaImage image) { + if (renderer == null) { + renderer = new PanoramaViewRenderer(image, context); + } else { + renderer.setImage(image); + } + } + + @Override + public void onNewFrame(HeadTransform headTransform) { + if (renderer == null) { + return; + } + // Store the head view matrix. + headTransform.getHeadView(headViewMatrix, 0); + + // Use the Euler angles to get the yaw. + float[] eulerAngles = new float[3]; + headTransform.getEulerAngles(eulerAngles, 0); + double yaw = eulerAngles[1]; // Yaw is the second element. + + // The heading offset is used to rotate the panorama when a new one is loaded. + renderer.setHeadingOffset((float) Math.toDegrees(yaw)); + } + + @Override + public void onDrawEye(CardboardView.Eye eye) { + if (renderer == null) { + return; + } + + // Apply the head view matrix to the eye. This is the crucial step. + eye.applyHeadView(headViewMatrix); + + // Set the model view and projection matrices for this eye. + renderer.setModelViewMatrix(eye.getEyeView()); + renderer.setProjectionMatrix(eye.getPerspective(Z_NEAR, Z_FAR)); + + renderer.onDrawFrame((GL10) null); + } + + @Override + public void onFinishFrame(Viewport viewport) { + // No need to do anything here. + } + + @Override + public void onSurfaceChanged(int width, int height) { + if (renderer == null) { + return; + } + + renderer.onSurfaceChanged((GL10) null, width, height); + } + + @Override + public void onSurfaceCreated(EGLConfig config) { + if (renderer == null) { + return; + } + + renderer.onSurfaceCreated((GL10) null, config); + } + + @Override + public void onRendererShutdown() { + renderer = null; + context = null; + } +} diff --git a/paperscope/photosphere/assets/photosphere/no_photo_sphere_found.jpg b/paperscope/photosphere/assets/photosphere/no_photo_sphere_found.jpg new file mode 100644 index 00000000..a9eb1300 Binary files /dev/null and b/paperscope/photosphere/assets/photosphere/no_photo_sphere_found.jpg differ diff --git a/paperscope/photosphere/lightcycle/Constants.java b/paperscope/photosphere/lightcycle/Constants.java new file mode 100644 index 00000000..09932188 --- /dev/null +++ b/paperscope/photosphere/lightcycle/Constants.java @@ -0,0 +1,47 @@ +// Copyright 2012 Google Inc. All Rights Reserved. + +package com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle; + +/** + * Commonly used constants for LightCycle. + * + * @author haeberling@google.com (Sascha Haeberling) + */ +public class Constants { + /** The width of the generated thumbnails in pixels. */ + public static final int THUMBNAIL_WIDTH = 1000; + + /** The aspect ratio of the generated thumbnails. */ + public static final float THUMBNAIL_ASPECT_RATIO = 4.0f; + + /** The desired preview frame width in pixels. */ + public static final int DESIRED_PREVIEW_IMAGE_WIDTH = 320; + + /** + * The maximum number of pixels allowed in a preview image. Size is limited to + * make optical flow in gyro bias calibration feasible. + */ + public static final int MAX_PREVIEW_IMAGE_PIXELS = 500000; + + /** The desired width in pixels for the captured images. **/ + public static final int DESIRED_PICTURE_IMAGE_WIDTH = 3000; + + /** + * The desired aspect ratio (width / height) for both the preview and + * full-sized photos. + **/ + public static final double DESIRED_WIDTH_TO_HEIGHT_RATIO = 4.0 / 3.0; + + /** Rendering color constants */ + public static final float[] GRAY = {0.4196f, 0.4196f, 0.4196f, 1.0f}; + public static final float[] TRANSPARENT_GRAY = + {0.4196f, 0.4196f, 0.4196f, 0.5f}; + public static final float[] GREEN = {0.0f, 1.0f, 0.0f, 1.0f}; + public static final float[] ANDROID_BLUE = {0.2f, 0.71f, 0.898f, 1.0f}; + public static final float[] ANDROID_GRAY = {0.8f, 0.8f, 0.8f, 1.0f}; + public static final float[] BACKGROUND_BLACK = {0.0f, 0.0f, 0.0f, 1.0f}; + public static final float[] WHITE = {1.0f, 1.0f, 1.0f, 1.0f}; + public static final float[] TRANSPARENT_WHITE = {1.0f, 1.0f, 1.0f, 0.6f}; + public static final float[] GROUND_PLANE_COLOR = {1.0f, 1.0f, 1.0f, 0.1f}; + +} diff --git a/paperscope/photosphere/lightcycle/README b/paperscope/photosphere/lightcycle/README new file mode 100644 index 00000000..cd99d719 --- /dev/null +++ b/paperscope/photosphere/lightcycle/README @@ -0,0 +1,4 @@ +This is a cut-down, modified copy of the google3 Lightcycle code +(see go/panoramas) used by the paperscope project. + +TODO(dcoz): merge changes back into the original LightCycle lib. diff --git a/paperscope/photosphere/lightcycle/math/PitchYaw.java b/paperscope/photosphere/lightcycle/math/PitchYaw.java new file mode 100644 index 00000000..aa43aac8 --- /dev/null +++ b/paperscope/photosphere/lightcycle/math/PitchYaw.java @@ -0,0 +1,18 @@ +// Copyright 2012 Google Inc. All Rights Reserved. + +package com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.math; + +/** + * Simple class containting pitch and yaw information. + * + * @author haeberling@google.com (Sascha Haeberling) + */ +public class PitchYaw { + public final float pitch; + public final float yaw; + + public PitchYaw(float pitch, float yaw) { + this.pitch = pitch; + this.yaw = yaw; + } +} diff --git a/paperscope/photosphere/lightcycle/math/Vector3.java b/paperscope/photosphere/lightcycle/math/Vector3.java new file mode 100644 index 00000000..897eadec --- /dev/null +++ b/paperscope/photosphere/lightcycle/math/Vector3.java @@ -0,0 +1,143 @@ +// Copyright 2011 Google Inc. All Rights Reserved. + +package com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.math; + + +/* + * Copyright (C) 2010 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. + */ + +/** Simple 3D vector class. Handles basic vector math for 3D vectors. */ +public final class Vector3 { + public float x; + public float y; + public float z; + + public static final Vector3 ZERO = new Vector3(0, 0, 0); + + public Vector3() { + } + + public Vector3(float xValue, float yValue, float zValue) { + set(xValue, yValue, zValue); + } + + public Vector3(Vector3 other) { + set(other); + } + + public final void add(Vector3 other) { + x += other.x; + y += other.y; + z += other.z; + } + + public final void add(float otherX, float otherY, float otherZ) { + x += otherX; + y += otherY; + z += otherZ; + } + + public final void subtract(Vector3 other) { + x -= other.x; + y -= other.y; + z -= other.z; + } + + public final void multiply(float magnitude) { + x *= magnitude; + y *= magnitude; + z *= magnitude; + } + + public final void multiply(Vector3 other) { + x *= other.x; + y *= other.y; + z *= other.z; + } + + public final void divide(float magnitude) { + if (magnitude != 0.0f) { + x /= magnitude; + y /= magnitude; + z /= magnitude; + } + } + + public final void set(Vector3 other) { + x = other.x; + y = other.y; + z = other.z; + } + + public final void set(float xValue, float yValue, float zValue) { + x = xValue; + y = yValue; + z = zValue; + } + + public final float dot(Vector3 other) { + return (x * other.x) + (y * other.y) + (z * other.z); + } + + public final float length() { + return (float) Math.sqrt(length2()); + } + + public final float length2() { + return (x * x) + (y * y) + (z * z); + } + + public final float distance2(Vector3 other) { + float dx = x - other.x; + float dy = y - other.y; + float dz = z - other.z; + return (dx * dx) + (dy * dy) + (dz * dz); + } + + public final float normalize() { + final float magnitude = length(); + + if (magnitude != 0.0f) { + x /= magnitude; + y /= magnitude; + z /= magnitude; + } + + return magnitude; + } + + public final void zero() { + set(0.0f, 0.0f, 0.0f); + } + + @Override + public String toString() { + return "" + x + ", " + y + ", " + z; + } + + public float[] toFloatArray() { + return new float[]{x, y, z}; + } + + public Vector3 crossProduct(Vector3 v) { + Vector3 r = new Vector3(); + r.x = y * v.z - z * v.y; + r.y = z * v.x - x * v.z; + r.z = x * v.y - y * v.x; + return r; + } + +} diff --git a/paperscope/photosphere/lightcycle/opengl/CurvedTile.java b/paperscope/photosphere/lightcycle/opengl/CurvedTile.java new file mode 100644 index 00000000..378316d3 --- /dev/null +++ b/paperscope/photosphere/lightcycle/opengl/CurvedTile.java @@ -0,0 +1,187 @@ +// Copyright 2012 Google Inc. All Rights Reserved. + +package com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.opengl; + +import android.opengl.GLES20; +import com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.math.Vector3; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.ShortBuffer; + +/** + * Contains one texture as well as all the vertices the texture is mapped on. In + * the simplest case, this is just a square, but usually contains a bunch of + * vertices to e.g. describe a part of a sphere the texture should be mapped + * onto. + * + * TODO(haeberling): This should ideally be a DrawableGL. + * + * @author haeberling@google.com (Sascha Haeberling) + */ +public class CurvedTile { + /** The ID of the texture that is assigned to this tile. */ + public final int textureId; + + private final int numVertices; + private final int numIndices; + + private int vertIndex = 0; + private FloatBuffer vertices; + private FloatBuffer texCoords; + private ShortBuffer indices; + + /** + * This contains all the vertices of this tile. The data is the same as in + * 'vertices' but optimized for faster access and normalized in order to do + * culling quickly. + */ + private Vector3[] normVertexArray; + + // TODO: b/352536125 - Remove this field for it is never read. + private boolean visible = false; + + /** + * Instantiates a new {@link CurvedTile} instance. + * + * @param textureId the ID of the texture to use. + * @param tesselationFactor defines by how much the geometry of the tile is + * sub-divided. A factor of 1 means there is no sub-division as a tile + * has 2x2 vertices, 2 means 3x3, 3 means 4x4 etc. + */ + public CurvedTile(int textureId, int tesselationFactor) { + this.textureId = textureId; + + final int verticesPerEdge = tesselationFactor + 1; + this.numVertices = verticesPerEdge * verticesPerEdge; + + // 3 = vertices per triangle, 2 = trianges per quad. + this.numIndices = (verticesPerEdge - 1) * (verticesPerEdge - 1) * 3 * 2; + this.initBuffers(); + + // Add the indices. + int index = 0; + for (int y = 0; y < verticesPerEdge - 1; ++y) { + int rowStart = y * verticesPerEdge; + for (int x = 0; x < verticesPerEdge - 1; ++x) { + int vertexIndex = rowStart + x; + + /** + * Add two triangles to define a quad. Given the following indices, the + * order in which they are added is: 0, 1, 2 2, 1, 3. + * + *

+         (2)-----(3)
+          | \     |
+          |  \    |
+          |   \   |
+          |    \  |
+          |     \ |
+         (0)-----(1)
+         
+ */ + this.indices.put(index++, (short) (vertexIndex + 0)); + this.indices.put(index++, (short) (vertexIndex + 1)); + this.indices.put(index++, (short) (vertexIndex + verticesPerEdge)); + this.indices.put(index++, (short) (vertexIndex + verticesPerEdge)); + this.indices.put(index++, (short) (vertexIndex + 1)); + this.indices.put(index++, (short) (vertexIndex + verticesPerEdge + 1)); + } + } + + // Add the texture coordinates. The texture will span the whole geometry, so + // texture coordinates are simply linearly interpolated. + index = 0; + for (int y = 0; y < verticesPerEdge; ++y) { + for (int x = 0; x < verticesPerEdge; ++x) { + float u = x / (float) (verticesPerEdge - 1); + float v = 1f - (y / (float) (verticesPerEdge - 1)); + this.texCoords.put(index++, u); + this.texCoords.put(index++, v); + } + } + } + + /** + * Sets the given vertices as the vertex buffer of this instance. Removes all + * previously added vertices. + */ + public void setVertices(Vertex[] vertices) { + // Each vertex is 3 coordinates of 4 bytes (float) + this.vertices = ByteBuffer.allocateDirect(numVertices * 3 * 4) + .order(ByteOrder.nativeOrder()).asFloatBuffer(); + vertIndex = 0; + normVertexArray = new Vector3[vertices.length]; + + for (int i = 0; i < vertices.length; ++i) { + Vertex vertex = vertices[i]; + vertex.addToBuffer(this.vertices, vertIndex); + vertIndex += 3; + + // Normalize so we can use the dot product in a range from -1 to 1 for + // visibility testing later. + Vector3 vector = new Vector3(vertex.x, vertex.y, vertex.z); + vector.normalize(); + normVertexArray[i] = vector; + } + } + + /** + * Draws this instance using the given shader. + */ + public void draw(Shader shader) { + this.vertices.position(0); + shader.setVertices(this.vertices); + + this.texCoords.position(0); + shader.setTexCoords(this.texCoords); + + this.indices.position(0); + // TODO(haeberling): Use GL_TRIANGLE_STRIP at some point, for improved + // performance. + GLES20.glDrawElements(GLES20.GL_TRIANGLES, this.numIndices, + GLES20.GL_UNSIGNED_SHORT, this.indices); + } + + /** + * Determines and sets the visibility of this tile based on the look-at vector + * of the scene and the minimum visibility dot product. + * + * @param lookAt the normalized look-at vector. + * @param minVisibilityDot the minimum visibility dot product between the + * look-at vector and a vertex so that the vertex is considered to be + * in view. + */ + public void determineVisibility(Vector3 lookAt, float minVisibilityDot) { + this.visible = false; + + for (int i = 0; i < this.normVertexArray.length; ++i) { + if (this.normVertexArray[i].dot(lookAt) >= minVisibilityDot) { + this.visible = true; + return; + } + } + } + + /** + * @return Whether this tile is visible. + */ + public boolean isVisible() { + // NOTE(dcoz): Consider that tiles are always visible to avoid flickering effects with wider + // field of view. We may want to revisit this if memory taken by textures is an issue. + // return this.visible; + return true; + } + + /** + * Initializes the buffers. + */ + private void initBuffers() { + // Each texture coordinate is 2 coordinates of 4 bytes + this.texCoords = ByteBuffer.allocateDirect(numVertices * 2 * 4) + .order(ByteOrder.nativeOrder()).asFloatBuffer(); + this.indices = ByteBuffer.allocateDirect(numIndices * 2) + .order(ByteOrder.nativeOrder()).asShortBuffer(); + this.normVertexArray = new Vector3[0]; + } +} diff --git a/paperscope/photosphere/lightcycle/opengl/DelayedTextureLoader.java b/paperscope/photosphere/lightcycle/opengl/DelayedTextureLoader.java new file mode 100644 index 00000000..a09a2f17 --- /dev/null +++ b/paperscope/photosphere/lightcycle/opengl/DelayedTextureLoader.java @@ -0,0 +1,184 @@ +// Copyright 2013 Google Inc. All Rights Reserved. + +package com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.opengl; + +import android.graphics.Bitmap; +import android.util.Log; +import com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.opengl.GLTexture.TextureType; +import com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.util.Callback; +import java.util.concurrent.Semaphore; +import org.jspecify.annotations.Nullable; + +/** + * Loads a texture when requested. The loading itself is done asynchronously. + * + * @author haeberling@google.com (Sascha Haeberling) + */ +public abstract class DelayedTextureLoader implements TextureProvider { + private static final String TAG = DelayedTextureLoader.class.getSimpleName(); + + /** The fade-in duration in milliseconds. */ + private static final long FADE_IN_DURATION_MILLIS = 200; + + /** Limits the number of active texture bitmap loading/decoding processes. */ + private final Semaphore semaphore; + + private final Callback triggerRenderCallback; + + private GLTexture glTexture; + private Bitmap bitmap; + private boolean loading = false; + private Thread bitmapLoadingThread; + + /** We use this to slowly fade in newly loaded textures. */ + private float alpha = 0; + + /** Timestamp of the fade-in start. */ + private long startFadeInTimeMillis = -1; + + /** + * Instantiates a new {@link DelayedTextureLoader}. + * + * @param bitmapLoadingSemaphore a shared semaphore to limit the amount of + * bitmaps being loaded. + * @param triggerRenderCallback notified when rendering is required (i.e. a + * bitmap finished loading and needs to be pushed to video memory or a + * fade-in is in progress. + */ + public DelayedTextureLoader( + Semaphore bitmapLoadingSemaphore, Callback triggerRenderCallback) { + semaphore = bitmapLoadingSemaphore; + this.triggerRenderCallback = triggerRenderCallback; + } + + /** + * If this is the first time getTexture is called, this will trigger the asynchronous loading of + * the texture bitmap. While the bitmap is loading this method will return null. + * + *

Once the texture bitmap is loaded, the next call to this method will create the texture + * object and return it. All subsequent calls will then return the previously created texture. + * + * @return Either a texture or null, of the texture is not loaded yet. + */ + @Override + public @Nullable GLTexture getTexture() { + // If the texture is loaded, return it. Nothing more to do. + if (glTexture != null) { + return glTexture; + } + + // Return null while loading the bitmap. + if (loading) { + return null; + } + loading = true; + + // A thread that waits for a semaphore permit to become available + // to load the bitmap. + bitmapLoadingThread = + new Thread() { + @Override + public void run() { + super.run(); + try { + semaphore.acquire(); + } catch (InterruptedException e) { + // Return without loading the bitmap. + return; + } + bitmap = loadBitmap(); + bitmapLoadingThread = null; + triggerRenderCallback.onCallback(null); + } + }; + bitmapLoadingThread.start(); + return null; + } + + /** + * If this loader has a pending bitmap loader thread, it will be cancelled. + */ + public void cancel() { + if (bitmapLoadingThread != null) { + bitmapLoadingThread.interrupt(); + bitmapLoadingThread = null; + bitmap = null; + } + } + + /** + * Recycles the texture. + */ + @Override + public void recycle() { + if (glTexture != null) { + glTexture.recycle(); + glTexture = null; + alpha = 0; + startFadeInTimeMillis = -1; + } + } + + @Override + public void createTexture() { + if (bitmap != null) { + createTextureAndReleaseBitmap(); + } + } + + @Override + public float getAlphaForRendering() { + // No texture yet. + if (glTexture == null) { + return 0; + } + + // Fade-in complete. + if (alpha == 1) { + return 1; + } + + // First time this method is called with a valid texture being present marks + // the starting time. + if (startFadeInTimeMillis < 0) { + startFadeInTimeMillis = System.currentTimeMillis(); + } + + alpha = (System.currentTimeMillis() - startFadeInTimeMillis) / (float) FADE_IN_DURATION_MILLIS; + alpha = Math.min(alpha, 1); + + // Make sure we re-render as long as the tile has not been faded in + // completely. + triggerRenderCallback.onCallback(null); + return alpha; + } + + /** + * Subclasses need to implement this method to load the texture bitmap. + * + * @return The texture bitmap. + */ + protected abstract Bitmap loadBitmap(); + + /** + * Loads the bitmap into video memory and then recycles the bitmap to free up + * main memory. Also releases the semaphore. + */ + private void createTextureAndReleaseBitmap() { + glTexture = new GLTexture(TextureType.Standard); + try { + // TODO(haeberling): Modify this to use texSubImage2D instead of + // texImage2D to prevent potential framerate drops. + glTexture.loadBitmap(bitmap); + } catch (OpenGLException e) { + Log.e(TAG, "Could not load texture"); + } finally { + // Once the bitmap is pushed to graphics memory, recycle it and + // release the semaphore. + semaphore.release(); + bitmap.recycle(); + bitmap = null; + loading = false; + } + } +} diff --git a/paperscope/photosphere/lightcycle/opengl/DrawableGL.java b/paperscope/photosphere/lightcycle/opengl/DrawableGL.java new file mode 100644 index 00000000..9f9cb4b2 --- /dev/null +++ b/paperscope/photosphere/lightcycle/opengl/DrawableGL.java @@ -0,0 +1,213 @@ +// Copyright 2010 Google Inc. All Rights Reserved. + +package com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.opengl; + +import android.opengl.Matrix; +import com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.math.Vector3; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.ShortBuffer; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Vector; + +/** + * Base class for all objects to be drawn. Each drawable contains storage for + * its geometry, textures, and the transformation for its orientation in space. + * + * Each drawable can hold a shader reference to be used for drawing. + * + * Drawables can contain children to construct a scene graph. Each child is + * drawn in the reference frame of the parent. + * + * @author settinger@google.com (Scott Ettinger) + * + */ +public abstract class DrawableGL { + // Geometry and texture storage. + protected FloatBuffer mVertices = null; + protected FloatBuffer mTexCoords = null; + protected ShortBuffer mIndices = null; + + /** Storage for textures. */ + protected Vector mTextures = new Vector(); + + /** Transformation. */ + protected float[] mLocalMatrix = new float[16]; + protected float[] mGlobalMatrix = new float[16]; + + /** Shader. */ + protected Shader mShader = null; + + /** Graph tree. */ + protected HashSet mChildren = null; + + /** Basic state of the drawable. */ + protected final DrawableGL mParent; + + + // ------------------------- Functions ------------------------------------- + + public DrawableGL() { + Matrix.setIdentityM(mLocalMatrix, 0); + mParent = null; + } + + public DrawableGL(DrawableGL parent) { + Matrix.setIdentityM(mLocalMatrix, 0); + mParent = parent; + } + + // ----------------------------- Gets -------------------------------------- + + public int getNumTextures() { + return mTextures.size(); + } + + public Shader getShader() { + return mShader; + } + + // Get the transformation within the parent frame. + public float[] getMatrix() { + return mLocalMatrix; + } + + // ----------------------------- Sets -------------------------------------- + + public void setShader(Shader shader) { + mShader = shader; + } + + // ------------------------- Rendering methods ----------------------------- + + // All drawing of the object in the local coordinate frame is done here + public abstract void drawObject(float[] baseTransform) throws OpenGLException; + + /** Draw given a base transformation using the stored shader. */ + public void draw(float[] parentTransform) throws OpenGLException { + // Compute the global matrix (parent transform applied to local transform) + Matrix.multiplyMM(mGlobalMatrix, 0, parentTransform, 0, mLocalMatrix, 0); + // Draw all children + if (mChildren != null) { + Iterator iter = mChildren.iterator(); + while (iter.hasNext()) { + iter.next().draw(mGlobalMatrix); + } + } + // Draw myself. + drawObject(mGlobalMatrix); + } + + /** + * Draw given a base transformation using the stored shader. Also perform culling if applicable + * using the given normalized look-at vector and minimum visibility dot product. + */ + public void draw(float[] parentTransform, Vector3 lookAt, float minVisibilityDot) + throws OpenGLException { + cull(lookAt, minVisibilityDot); + draw(parentTransform); + } + + // ---------------------- Scene building methods --------------------------- + + // Add a child of the drawable. + public void addChild(DrawableGL child) { + if (mChildren == null) { + mChildren = new HashSet(); + } + mChildren.add(child); + } + + public void remove() { + // Remove children of this drawable. + if (mChildren != null) { + Iterator iter = mChildren.iterator(); + while (iter.hasNext()) { + iter.next().remove(); + } + } + // Recycle any textures + for (int i = 0; i < mTextures.size(); ++i) { + mTextures.get(i).recycle(); + } + + // Remove this drawable from its parent. + if (mParent != null) { + mParent.removeChild(this); + } + } + + // Remove a given child drawable from the hashset. + public void removeChild(DrawableGL child) { + if (mChildren != null) { + mChildren.remove(child); + } + } + + /** + * Allows sub-classes to cull geometry that is not visible. + * + * @param lookAt the normalized look-at vector. + * @param minVisibilityDot the minimum visibility dot product between the + * look-at vector and a vertex so that the vertex is considered to be + * in view. + */ + protected void cull(Vector3 lookAt, float minVisibilityDot) { + // Do nothing by default but allow sub-classes to override. + } + + /** + * Initializes the byte buffers for the vertices, texture coordinates and + * indices. + * + * @param numVertices number of vertices. + * @param numIndices number of indices. + * @param allocateTextureCoords if true allocate texture coordinates per + * vertex. + */ + protected void initGeometry( + int numVertices, int numIndices, boolean allocateTextureCoords) { + // Allocate buffer space. + // Each vertex is 3 coordinates of 4 bytes (float) + mVertices = ByteBuffer.allocateDirect(numVertices * 3 * 4) + .order(ByteOrder.nativeOrder()).asFloatBuffer(); + + mIndices = ByteBuffer.allocateDirect(numIndices * 2) + .order(ByteOrder.nativeOrder()).asShortBuffer(); + + if (allocateTextureCoords) { + // Each texture coordinate is 2 coordinates of 4 bytes + mTexCoords = ByteBuffer.allocateDirect(numVertices * 2 * 4) + .order(ByteOrder.nativeOrder()).asFloatBuffer(); + } + } + + /** + * Put an index into the vertex buffer. + * + * @param vertexIndex - the index of the vertex to be added (in vertices - not + * bytes or floats) + * @param x X coordinate. + * @param y Y coordinate. + * @param z Z coordinate. + */ + protected void putVertex(int vertexIndex, float x, float y, float z) { + int index = vertexIndex * 3; + mVertices.put(index++, x); + mVertices.put(index++, y); + mVertices.put(index++, z); + } + + /** + * Add a value to the index buffer. + * + * @param position position in the buffer. + * @param value index value to be added. + */ + protected void putIndex(int position, short value) { + mIndices.put(position, value); + } + +} diff --git a/paperscope/photosphere/lightcycle/opengl/GLTexture.java b/paperscope/photosphere/lightcycle/opengl/GLTexture.java new file mode 100644 index 00000000..db9b8023 --- /dev/null +++ b/paperscope/photosphere/lightcycle/opengl/GLTexture.java @@ -0,0 +1,139 @@ +// Copyright 2011 Google Inc. All Rights Reserved. + +package com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.opengl; + +import android.graphics.Bitmap; +import android.opengl.GLES20; +import android.opengl.GLUtils; + +/** + * OpenGL texture management object. It supports creating a texture from a + * bitmap. + * + * @author settinger@google.com (Scott Ettinger) + */ +public class GLTexture { + + /** + * Type to define the type of texture to create. A standard texture uses + * bilinear interpolation while a nearest neighbor texture does not. + */ + public enum TextureType { + Standard, NearestNeighbor + } + + private int textureIndex = -1; + + public GLTexture() {} + + // TODO(settinger): re-factor this to make creating a texture more consistent + // with one create function that takes parameters for + // interpolation and mip-mapping and takes an optional + // bitmap. + public GLTexture(TextureType type) { + textureIndex = + switch (type) { + case Standard -> GLTexture.createStandardTexture(); + case NearestNeighbor -> GLTexture.createNNTexture(); + default -> GLTexture.createStandardTexture(); + }; + } + + /** Get the OpenGL texture index. */ + public int getIndex() { + return textureIndex; + } + + /** Clear the texture and clear the associated resources. */ + public void recycle() { + int[] textures = new int[] {textureIndex}; + GLES20.glDeleteTextures(1, textures, 0); + textureIndex = -1; + } + + /** Bind the texture to the shader. */ + public void bind(Shader shader) throws OpenGLException { + if (textureIndex < 0) { + throw new OpenGLException("Trying to bind without a loaded texture"); + // return; + } + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureIndex); + OpenGLException.logError("glBindTexture"); + } + + /** Create a texture from a bitmap object. */ + public void loadBitmap(Bitmap bitmap) throws OpenGLException { + int[] textures = new int[1]; + GLES20.glGenTextures(1, textures, 0); + textureIndex = textures[0]; + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureIndex); + + // Use linear interpolation for up-sampling and down-sampling + GLES20.glTexParameterf( + GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameterf( + GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + + // Clamp all textures to their edge + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, + GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, + GLES20.GL_CLAMP_TO_EDGE); + + GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0); + OpenGLException.logError("Texture : loadBitmap"); + bitmap.recycle(); + } + + /** Create a texture with Linear interpolation sampling. */ + public static int createStandardTexture() { + int[] textures = new int[1]; + GLES20.glGenTextures(1, textures, 0); + + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]); + + // Disable MIP-mapping here. + GLES20.glTexParameterf( + GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST); + + // Use linear interpolation for up sampling + GLES20.glTexParameterf( + GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + + // Clamp all textures to their edge + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, + GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, + GLES20.GL_CLAMP_TO_EDGE); + return textures[0]; + } + + /** Create a texture with nearest neighbor sampling. */ + public static int createNNTexture() { + int[] textures = new int[1]; + GLES20.glGenTextures(1, textures, 0); + + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]); + + // Disable MIP-mapping here. + GLES20.glTexParameterf( + GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST); + + // Use linear interpolation for up sampling + GLES20.glTexParameterf( + GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST); + + // Clamp all textures to their edge + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, + GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, + GLES20.GL_CLAMP_TO_EDGE); + return textures[0]; + } + + /** Set the OpenGL texture id for this texture. */ + public void setIndex(int id) { + textureIndex = id; + } + +} diff --git a/paperscope/photosphere/lightcycle/opengl/OpenGLException.java b/paperscope/photosphere/lightcycle/opengl/OpenGLException.java new file mode 100644 index 00000000..b5cf3c04 --- /dev/null +++ b/paperscope/photosphere/lightcycle/opengl/OpenGLException.java @@ -0,0 +1,42 @@ +// Copyright 2011 Google Inc. All Rights Reserved. + +// Exception class for OpenGL routines. + +package com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.opengl; + +import android.opengl.GLES20; +import android.opengl.GLU; +import android.util.Log; + +/** + * @author settinger@google.com (Scott Ettinger) + * + * Class to handle Exceptions related to OpenGL. + * + */ +public class OpenGLException extends Exception { + + private static final String TAG = "LightCycle"; + public OpenGLException(String exception) { + super(exception); + Log.e(TAG, exception, this); + } + + public OpenGLException(String exception, String log) { + super(exception); + Log.e(TAG, exception + " : " + log, this); + } + + public static void vGL() throws OpenGLException { + logError(""); + } + + public static void logError(String op) throws OpenGLException { + int error; + while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) { + throw new OpenGLException(op + ": glError " + GLU.gluErrorString(error) + + " " + error); + } + } + +} diff --git a/paperscope/photosphere/lightcycle/opengl/PartialSphere.java b/paperscope/photosphere/lightcycle/opengl/PartialSphere.java new file mode 100644 index 00000000..ada2423b --- /dev/null +++ b/paperscope/photosphere/lightcycle/opengl/PartialSphere.java @@ -0,0 +1,297 @@ +// Copyright 2012 Google Inc. All Rights Reserved. + +package com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.opengl; + +import android.util.Log; +import com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.math.Vector3; +import com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.shaders.TransparencyShader; +import com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.util.DepthMap; +import com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.viewer.PanoramaImage; +import com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.viewer.TileProvider; +import java.util.ArrayList; +import java.util.List; + +/** + * A {@link DrawableGL} class to generate geometry and render a partial sphere. + * It can also render a full sphere, as a full sphere is one kind of a partial + * sphere. + * + * @author haeberling@google.com (Sascha Haeberling) + */ +public class PartialSphere extends DrawableGL { + private static final String TAG = PartialSphere.class.getSimpleName(); + + /** + * The maximum angle we allow between columns in the sphere to avoid + * artifacts. + * NOTE(dcoz): original value was 0.12f. Use more vertice to make better use of the depthmap data. + */ + private static final float MAX_ANGLE_STEP_RAD = 0.05f; + + /** The radius of the sphere. */ + private final float radius; + + /** The panorama image to display. */ + private PanoramaImage image; + + /** Provides the tiles from the original image. */ + private TileProvider tileProvider; + + /** All the curved tiles that make up this partial sphere. */ + private CurvedTile[][] curvedTiles; + + /** Creates texture providers. */ + private TextureLoaderManager textureLoaderManager; + + /** Limit culling rate. */ + private int cullCounter; + + /** + * Constructs a new partial sphere. + * + * @param radius the radius of the sphere. + * @param textureLoaderManager used to create the texture providers. + */ + public PartialSphere(float radius, TextureLoaderManager textureLoaderManager) { + this.radius = radius; + this.textureLoaderManager = textureLoaderManager; + } + + /** + * Sets a new image, loads its textures and generates the geometry. This needs + * to be called from the GL thread. + * + * @param image the image to display. + */ + public void setImage(PanoramaImage image, TextureLoaderManager textureLoaderManager) { + this.textureLoaderManager = textureLoaderManager; + this.textureLoaderManager.reset(); + this.cullCounter = -1; + this.image = image; + this.tileProvider = image.getTileProvider(); + this.setupTextureProviders(); + image.init(); + this.generateGeometry(radius); + } + + @Override + public void drawObject(float[] transform) throws OpenGLException { + // Select the shader. + mShader.bind(); + + // Draw all curved tiles. + for (int x = 0; x < curvedTiles.length; ++x) { + CurvedTile[] textureColumn = this.curvedTiles[x]; + for (int y = 0; y < textureColumn.length; ++y) { + CurvedTile tile = textureColumn[y]; + TextureProvider textureProvider = mTextures.get(tile.textureId); + + // If a bitmap has been loaded make sure a texture is created from it. + // Frees held semaphores in the delayed texture loaders if a bitmap + // is loaded but no texture was created yet. + textureProvider.createTexture(); + + // Don't render tiles that are not in view. + if (!tile.isVisible()) { + continue; + } + + GLTexture texture = textureProvider.getTexture(); + if (texture == null) { + // Texture can be null if it could not be loaded or is delay loaded. + continue; + } + texture.bind(mShader); + mShader.setTransform(transform); + + // Apply fade-in transparency for tiles. + float alpha = textureProvider.getAlphaForRendering(); + ((TransparencyShader) mShader).setAlpha(alpha); + tile.draw(mShader); + } + } + // Reset alpha back to 100%. + ((TransparencyShader) mShader).setAlpha(1); + } + + @Override + protected void cull(Vector3 lookAt, float minVisibilityDot) { + // Reduce CPU impact of culling by only calling it every 4th frame. + if (++cullCounter % 4 != 0) { + return; + } + cullCounter = 0; + + // Determine the visibility of each tile. + for (int x = 0; x < curvedTiles.length; ++x) { + CurvedTile[] textureColumn = this.curvedTiles[x]; + for (int y = 0; y < textureColumn.length; ++y) { + CurvedTile tile = textureColumn[y]; + tile.determineVisibility(lookAt, minVisibilityDot); + } + } + } + + /** + * Generates the geometry for the partial sphere. + * + * @param radius the radius of the sphere to generate. + */ + // TODO: b/352536125 - Revisit why radius is not used. + private void generateGeometry(float radius) { + final int tesselationFactor = + (int) Math.ceil(this.image.getTileSizeRad() / MAX_ANGLE_STEP_RAD); + Log.d(TAG, "tesselation factor: " + tesselationFactor); + + // The number of latitudes and longitudes of the partial sphere mesh. + int numLatitudes = + (this.tileProvider.getTileCountY()) * tesselationFactor + 1; + int numLongitudes = + (this.tileProvider.getTileCountX()) * tesselationFactor + 1; + + // The number of vertices and indices of the partial sphere. + int numVertices = numLatitudes * numLongitudes; + // Two triangles per quad = 6 indices. + int numIndices = (numLatitudes - 1) * (numLongitudes - 1) * 6; + this.initGeometry(numVertices, numIndices, true); + + // The angle between vertices of the mesh. The smaller the step, the finer + // the mesh. + float angleStepLat = this.image.getTileSizeRad() / tesselationFactor; + float angleStepLon = this.image.getTileSizeRad() / tesselationFactor; + + // On the edges we might have denser geometry due to smaller tiles. Hence we + // need to compute a separate angle step, which might be smaller. + float angleStepLatEdge = angleStepLat + * (this.image.getLastRowHeightRad() / this.image.getTileSizeRad()); + float angleStepLonEdge = angleStepLon + * (this.image.getLastColumnWidthRad() / this.image.getTileSizeRad()); + + // The offset is based on the meta-data of the pano. If the pano is cropped, + // we need to know where to put the pano. + final float offsetLat = this.image.getOffsetTopRad() + + this.image.getPanoHeightRad() - (float) (Math.PI / 2f); + final float offsetLon = -this.image.getOffsetLeftRad() - (float) Math.PI; + + // Up until the second to last tile column we size all columns equally. The + // last tile column can be denser in vertices though, if the texture is not + // divisible by the texture size. In case the texture is narrower, the + // geometry also needs to be made narrower/denser, which will happen for all + // vertex longitudes greater than this value. + int secondToLastTileEndColumn = numLongitudes - tesselationFactor - 1; + int secondTileStartRow = tesselationFactor; + + // Generate ALL the vertices! + final float piDiv2 = (float) Math.PI / 2; + final Vertex[][] vertices = new Vertex[numLongitudes][numLatitudes]; + for (int r = 0; r < numLatitudes; ++r) { + float lat; + + // The bottom most tile row might need to be denser if the tiles in this + // row are less tall than the normal tile size specifies. + if (r < secondTileStartRow) { + lat = r * angleStepLatEdge - offsetLat; + } else { + // All the rows above the first need to get lowered by the padding + // amount, but are spaced with the default angle step value. + lat = r * angleStepLat - offsetLat + - (this.image.getTileSizeRad() - this.image.getLastRowHeightRad()); + } + + for (int c = 0; c < numLongitudes; ++c) { + // All columns have the same angleStep... + float lon = c * angleStepLon; + + // ... except for the vertices of the last tile column, which might be + // denser. + if (c > secondToLastTileEndColumn) { + // As the base we calculate the longitude of the second to last tile + // column. + float prevLon = secondToLastTileEndColumn * angleStepLon; + + // We then use the (potentially smaller) angle step of the denser last + // tile column to calculate the longitudes. + lon = prevLon + ((c - secondToLastTileEndColumn) * angleStepLonEdge); + } + + // Apply the longitudal offset. + lon = lon - piDiv2 - offsetLon; + + // Compute vertex coordinates from the lat/long. + final float sinlat = (float) Math.sin(lat); + final float sinlon = (float) Math.sin(lon); + final float coslat = (float) Math.cos(lat); + final float coslon = (float) Math.cos(lon); + + // Push each vertex along the viewing ray according to its depth in the depthmap. + final float depth = getDepthAtLatLon(lat, lon); + final float x = coslat * coslon * depth; + final float y = sinlat * depth; + final float z = coslat * sinlon * depth; + + vertices[c][r] = new Vertex(x, y, z); + } + } + + // The number of textures we need for this partial sphere. + final int textureCountX = this.tileProvider.getTileCountX(); + final int textureCountY = this.tileProvider.getTileCountY(); + + // Create the curved tiles and assign the correct vertices to them. + curvedTiles = new CurvedTile[textureCountX][textureCountY]; + for (int y = 0, v = 0; v < textureCountY; y += tesselationFactor, ++v) { + for (int x = 0, u = 0; u < textureCountX; x += tesselationFactor, ++u) { + curvedTiles[u][v] = + new CurvedTile(v + (u * textureCountY), tesselationFactor); + List vertexList = new ArrayList(); + + // For each texture, add the relevant vertices. + // TODO(haeberling): We should store vertices in a single buffer for + // efficiency + for (int yy = 0; yy < tesselationFactor + 1; ++yy) { + for (int xx = 0; xx < tesselationFactor + 1; ++xx) { + vertexList.add(vertices[x + xx][y + yy]); + } + } + curvedTiles[u][v].setVertices(vertexList.toArray(new Vertex[0])); + } + } + } + + /** + * Returns depth for the image point located at (lat, lon) using the image depthmap. + * If there is no depthmap for that image, a constant depth is used. + */ + private float getDepthAtLatLon(float lat, float lon) { + DepthMap depthMap = image.getDepthMap(); + if (depthMap == null) { + return radius; + } + + // Convert our (lat, lon) coordinates to the normalized coordinate frame used by the depthmap. + lat = (float) (Math.PI / 2 - lat); + lon = (float) (lon - Math.PI / 2); + return depthMap.getDepth(lon / (Math.PI * 2), lat / (Math.PI)); + } + + /** Set up the delayed texture loaders. */ + private void setupTextureProviders() { + if (this.tileProvider == null) { + Log.e(TAG, "tile provider is null. Cannot load textures"); + return; + } + + // Recycle current textures if present. + for (TextureProvider texture : mTextures) { + texture.recycle(); + } + mTextures.clear(); + + // Create the delayed textures in order. + for (int x = 0; x < this.tileProvider.getTileCountX(); ++x) { + for (int y = 0; y < this.tileProvider.getTileCountY(); ++y) { + mTextures.add(textureLoaderManager.getDelayedLoader(x, y)); + } + } + } +} diff --git a/paperscope/photosphere/lightcycle/opengl/Shader.java b/paperscope/photosphere/lightcycle/opengl/Shader.java new file mode 100644 index 00000000..da2b95b5 --- /dev/null +++ b/paperscope/photosphere/lightcycle/opengl/Shader.java @@ -0,0 +1,201 @@ +// Copyright 2010 Google Inc. All Rights Reserved. + +// Shader base class. + +// Base class for shaders. It provides handle indices for vertices, +// texture coordinates, and transforms along with methods for connecting +// data buffers for rendering and for shader creation and binding. + +package com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.opengl; + +import android.content.Context; +import android.opengl.GLES20; +import java.io.IOException; +import java.io.InputStream; +import java.nio.FloatBuffer; + +/** + * @author settinger@google.com (Scott Ettinger) + * + * OpenGL shader base class object. It handles the common operations for + * shaders. + * + */ +public abstract class Shader { + + // Attribute Handles. + protected int mVertexIndex = -1; + protected int mTextureCoordIndex = -1; + protected int mTransformIndex = -1; + protected int mSamplerIndex = -1; + + // Shader program index. + protected int mProgram; + + // ----------- Gets for handle indices --------------------- + + public int getVertexIndex() { + return mVertexIndex; + } + + public int getTextureCoordIndex() { + return mTextureCoordIndex; + } + + public int getTransformIndex() { + return mTransformIndex; + } + + public int getSamplerIndex() { + return mSamplerIndex; + } + + // ------------------- Sets for rendering data --------------------------- + + public void setVertices(FloatBuffer vertices) { + // If the shader does not take vertices as input then exit. + if (mVertexIndex < 0) { + return; + } + + // Set the vertex data. + GLES20.glVertexAttribPointer(mVertexIndex, 3, GLES20.GL_FLOAT, + false, 4 * 3, vertices); + GLES20.glEnableVertexAttribArray(mVertexIndex); + } + + public void setTexCoords(FloatBuffer texCoords) { + // If the shader does not take texture coordinates as input then exit. + if (mTextureCoordIndex < 0) { + return; + } + + // Set the texture coordinate data. + GLES20.glVertexAttribPointer(mTextureCoordIndex, 2, GLES20.GL_FLOAT, false, + 0, texCoords); + GLES20.glEnableVertexAttribArray (mTextureCoordIndex); + } + + public void setTransform(float[] transform) { + // If the shader does not take texture coordinates as input then exit. + if (mTransformIndex < 0) { + return; + } + + GLES20.glUniformMatrix4fv(mTransformIndex, 1, false, transform, 0); + } + + // ------------------------ Misc ---------------------------------------- + + // Activate the default texture unit. + public void activateTexture() { + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + } + + // Activate the shader + public void bind() { + GLES20.glUseProgram(mProgram); + } + + // De-activate the shader. + public void unbind() { + GLES20.glUseProgram(0); + } + + // -------------------- Shader Creation methods ---------------------------- + + // Create a shader program from source strings. + public static int createProgram(String vertexSource, String fragmentSource) + throws OpenGLException { + int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource); + int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource); + + int program = GLES20.glCreateProgram(); + if (program == 0) { + throw new OpenGLException("Unable to create program"); + } + GLES20.glAttachShader(program, vertexShader); + OpenGLException.logError("glAttachShader"); + GLES20.glAttachShader(program, pixelShader); + OpenGLException.logError("glAttachShader"); + GLES20.glLinkProgram(program); + int[] linkStatus = new int[1]; + GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0); + + if (linkStatus[0] != GLES20.GL_TRUE) { + GLES20.glDeleteProgram(program); + throw new OpenGLException("Could not link program", + GLES20.glGetProgramInfoLog(program)); + } + return program; + } + + // Load the shader to the hardware. + protected static int loadShader(int shaderType, + String source) throws OpenGLException { + int shader = GLES20.glCreateShader(shaderType); + if (shader == 0) { + throw new OpenGLException("Unable to create shader"); + } + + GLES20.glShaderSource(shader, source); + GLES20.glCompileShader(shader); + int[] compiled = new int[1]; + GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0); + if (compiled[0] == 0) { + String shaderInfoLog = GLES20.glGetShaderInfoLog(shader); + GLES20.glDeleteShader(shader); + throw new OpenGLException("Unable to compile shader " + shaderType, + shaderInfoLog); + } + return shader; + } + + // Get a given shader attribute index. + protected int getAttribute(int program, String name) throws OpenGLException { + int handle = GLES20.glGetAttribLocation(program, name); + if (handle == -1) { + throw new OpenGLException("Unable to find " + name + " in shader"); + } + OpenGLException.logError("glGetAttribLocation " + name); + return handle; + } + + // Get a given shader uniform index. + protected int getUniform(int program, String name) throws OpenGLException { + int handle = GLES20.glGetUniformLocation(program, name); + if (handle == -1) { + throw new OpenGLException("Unable to find " + name + " in shader"); + } + OpenGLException.logError("glGetUniformLocation " + name); + return handle; + } + + // Create a program given resource data IDs containing shader source code. + public static int createProgram(Context context, + int vertexSourceId, + int fragmentSourceId) throws OpenGLException { + return createProgram( + getRawResourceString(context, vertexSourceId), + getRawResourceString(context, fragmentSourceId)); + } + + // Get a string from a raw resource + protected static String getRawResourceString(Context context, + int resourceId) { + InputStream ins = context.getResources().openRawResource(resourceId); + StringBuilder buffer = new StringBuilder(); + try { + // TODO(settinger): fix this to not use a fixed buffer size. + byte[] tmp = new byte[1024]; + for (int i; (i = ins.read(tmp)) != -1;) { + buffer.append(new String(tmp, 0, i)); + } + return buffer.toString(); + } catch (IOException e) { + e.printStackTrace(); + return new String(""); + } + } + +} diff --git a/paperscope/photosphere/lightcycle/opengl/SimpleTextureProvider.java b/paperscope/photosphere/lightcycle/opengl/SimpleTextureProvider.java new file mode 100644 index 00000000..d5831d7c --- /dev/null +++ b/paperscope/photosphere/lightcycle/opengl/SimpleTextureProvider.java @@ -0,0 +1,36 @@ +// Copyright 2013 Google Inc. All Rights Reserved. + +package com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.opengl; + +/** + * A simple provider which wraps around the given GLTexture. + * + * @author haeberling@google.com (Sascha Haeberling) + */ +public class SimpleTextureProvider implements TextureProvider { + private final GLTexture texture; + + public SimpleTextureProvider(GLTexture texture) { + this.texture = texture; + } + + @Override + public GLTexture getTexture() { + return texture; + } + + @Override + public void recycle() { + texture.recycle(); + } + + @Override + public void createTexture() { + // Nothing to do. + } + + @Override + public float getAlphaForRendering() { + return 1; + } +} diff --git a/paperscope/photosphere/lightcycle/opengl/Sprite.java b/paperscope/photosphere/lightcycle/opengl/Sprite.java new file mode 100644 index 00000000..fd66b687 --- /dev/null +++ b/paperscope/photosphere/lightcycle/opengl/Sprite.java @@ -0,0 +1,527 @@ +// Copyright 2011 Google Inc. All Rights Reserved. + +package com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.opengl; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Point; +import android.opengl.GLES20; +import android.opengl.Matrix; +import android.util.Log; +import com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.util.LG; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Sprite object that can be rendered in either 2D or 3D. + * + * @author settinger@google.com (Scott Ettinger) + */ +public class Sprite extends DrawableGL { + + private static final String TAG = "LightCycle"; + private float depth = 4.0f; + + private Point imageDim = new Point(); + private float halfX; + private float halfY; + private float[] transform = new float[16]; + private float[] objectTransform = new float[16]; + private int numIndices; + private int numVertices; + private boolean initialized = false; + + public int getWidth() { + return imageDim.x; + } + + public int getHeight() { + return imageDim.y; + } + + /** + * Set the texture to be used to render the sprite. + * + * @param texture GLTexture object to be used for rendering. + */ + public void setTexture(GLTexture texture) { + mTextures.set(0, new SimpleTextureProvider(texture)); + } + + private void createRenderData() { + // Only 2 triangles. + numIndices = 6; + numVertices = 4; + + // Allocate buffer space. + mVertices = + ByteBuffer.allocateDirect(numVertices * 3 * 4) + .order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + mTexCoords = + ByteBuffer.allocateDirect(numVertices * 2 * 4) + .order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + mIndices = + ByteBuffer.allocateDirect(numIndices * 2).order(ByteOrder.nativeOrder()).asShortBuffer(); + + mVertices.clear(); + mTexCoords.clear(); + mIndices.clear(); + + halfX = imageDim.x / 2.0f; + halfY = imageDim.y / 2.0f; + + // Create texture coordinates. + float[] texCoordData = {0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f}; + + for (int i = 0; i < texCoordData.length; ++i) { + mTexCoords.put(i, texCoordData[i]); + } + + // Create the triangle indices. + short[] indices = {0, 1, 2, 0, 2, 3}; + for (int i = 0; i < indices.length; ++i) { + mIndices.put(i, indices[i]); + } + + // Set the initial sprite transform to identity + Matrix.setIdentityM(objectTransform, 0); + } + + private boolean initFromDrawable(Context context, int drawable) { + // Create textures + GLTexture texture = new GLTexture(); + mTextures.add(0, new SimpleTextureProvider(texture)); + + // Load the image and create the texture. + BitmapFactory.Options opts = new BitmapFactory.Options(); + opts.inScaled = false; + Bitmap bitmap1 = + BitmapFactory.decodeResource(context.getResources(), drawable, opts); + if (bitmap1 == null) { + return false; + } + + imageDim.set(bitmap1.getWidth(), bitmap1.getHeight()); + + try { + mTextures.get(0).getTexture().loadBitmap(bitmap1); + } catch (OpenGLException e) { + e.printStackTrace(); + } + bitmap1.recycle(); + + // Create all geometry and texture coordinates for rendering the sprite. + createRenderData(); + + return true; + } + + private boolean initFromBitmap(Bitmap bitmap) { + // Create texture. + GLTexture texture = new GLTexture(); + mTextures.add(0, new SimpleTextureProvider(texture)); + imageDim.set(bitmap.getWidth(), bitmap.getHeight()); + try { + mTextures.get(0).getTexture().loadBitmap(bitmap); + } catch (OpenGLException e) { + e.printStackTrace(); + return false; + } + + // Create all geometry and texture coordinates for rendering the sprite. + createRenderData(); + return true; + } + + /** + * Set the current transform. + * + * @param transform + */ + public void setTransform(float[] transform) { + objectTransform = transform; + } + + /** + * Initialize to be drawn as a 2 dimensional image (orthographic). The image will be equal in size + * in pixels to the given drawable. + * + * @param context - Android context. + * @param drawable - bitmap to use as the sprite. + * @param depth - depth for the sprite to be drawn. + * @param scale - scale that the sprite will be drawn relative to the actual bitmap size. + * @return true if initialization was successful. + */ + public boolean init2D(Context context, int drawable, float depth, float scale) { + + if (!initFromDrawable(context, drawable)) { + return false; + } + this.depth = depth; + halfX *= scale; + halfY *= scale; + float[] vertexData = { + -halfX, + halfY, + this.depth, + halfX, + halfY, + this.depth, + halfX, + -halfY, + this.depth, + -halfX, + -halfY, + this.depth + }; + + for (int i = 0; i < vertexData.length; ++i) { + mVertices.put(i, vertexData[i]); + } + initialized = true; + return true; + } + + /** + * Initialize to be drawn as a 2 dimensional image (orthographic). The image + * will be equal in size in pixels to the given bitmap. + * + * @param bitmap - bitmap to use as the sprite. + * @param depth - depth for the sprite to be drawn. + * @param scale - scale that the sprite will be drawn relative to the actual + * bitmap size. + * @return true if initialization was successful. + */ + public boolean init2D(Bitmap bitmap, float depth, float scale) { + + initFromBitmap(bitmap); + this.depth = depth; + halfX *= scale; + halfY *= scale; + float[] vertexData = { + -halfX, + halfY, + this.depth, + halfX, + halfY, + this.depth, + halfX, + -halfY, + this.depth, + -halfX, + -halfY, + this.depth + }; + + for (int i = 0; i < vertexData.length; ++i) { + mVertices.put(i, vertexData[i]); + } + initialized = true; + return true; + } + + /** + * Initialize the sprite to be drawn as a 2 dimensional image (orthographic) + * given only its dimensions and depth. Before being drawn, a texture must be + * specified by setTexture(); + * + * @param width + * @param height + * @param depth + */ + public void init2D(int width, int height, float depth) { + imageDim.set(width, height); + createRenderData(); + + // Create the vertex coordinates. + halfX = width / 2.0f; + halfY = height / 2.0f; + this.depth = depth; + float[] vertexData = { + -halfX, + halfY, + this.depth, + halfX, + halfY, + this.depth, + halfX, + -halfY, + this.depth, + -halfX, + -halfY, + this.depth + }; + + for (int i = 0; i < vertexData.length; ++i) { + mVertices.put(i, vertexData[i]); + } + initialized = true; + } + + /** + * Create the sprite to be drawn in 3-D as a flat surface. The scale is given + * by the size that the larger of width or height will be scaled to in 3space. + * + * @param context - Android context + * @param drawable - bitmap to use as the sprite. + * @param largestDimension - the size in 3-space of the largest side of the + * sprite. + * @param depth - depth for the sprite to be drawn. + * @return true if the sprite is successfully initialized. + */ + public boolean init3D( + Context context, int drawable, float largestDimension, float depth) { + + if (!initFromDrawable(context, drawable)) { + return false; + } + + float xdim; + float ydim; + if (imageDim.x > imageDim.y) { + xdim = largestDimension; + ydim = largestDimension * imageDim.y / imageDim.x; + } else { + ydim = largestDimension; + xdim = largestDimension * imageDim.x / imageDim.y; + } + + this.depth = depth; + float x2 = xdim / 2; + float y2 = ydim / 2; + float[] vertexData = { + -x2, y2, this.depth, x2, y2, this.depth, x2, -y2, this.depth, -x2, -y2, this.depth + }; + + for (int i = 0; i < vertexData.length; ++i) { + mVertices.put(i, vertexData[i]); + } + initialized = true; + return true; + } + + /** + * Draw the sprite using the base class draw function. This function will bind + * the shader upon every call. For drawing multiple sprites, call + * drawNoBind(). + */ + @Override + public void drawObject(float[] transform) throws OpenGLException {} + + /** + * Draw the sprite in 2-D with u and v as the top, left coordinates of the + * sprite. The transform parameter should be an orthographic transform with + * 1:1 pixel units. + * + * @param transform - 4x4 transform matrix + * @param u - left edge of sprite. + * @param v - top edge of sprite. + * @throws OpenGLException + */ + public void draw(float[] transform, float u, float v) throws OpenGLException { + + if (!initialized) { + Log.e(TAG, "Sprite not initialized."); + } + + // Select the shader. + mShader.bind(); + + // Set shader and geometry. + mVertices.position(0); + mTexCoords.position(0); + mShader.setVertices(mVertices); + mShader.setTexCoords(mTexCoords); + Matrix.translateM(this.transform, 0, transform, 0, u + halfX, v + halfY, 0); + mShader.setTransform(this.transform); + + if (mTextures.isEmpty()) { + LG.d("Error : no textures defined for Sprite"); + return; + } + mTextures.get(0).getTexture().bind(mShader); + + // Draw the message. + mIndices.position(0); + GLES20.glDrawElements(GLES20.GL_TRIANGLES, numIndices, GLES20.GL_UNSIGNED_SHORT, mIndices); + } + + /** + * Draw the sprite in 2-D with u and v as the top, left coordinates of the + * sprite. The transform parameter should be an orthographic transform with + * 1:1 pixel units. + * + * @param transform 4x4 transform matrix + * @param u center U coordinate of the sprite. + * @param v center V coordinate of the sprite. + * @param rotationAngleDegrees the rotation angle for the sprite to be drawn. + * The sprite will be rotated about its center. + * @throws OpenGLException + */ + public void drawRotated(float[] transform, float u, float v, + float rotationAngleDegrees, float scale) throws OpenGLException { + + if (!initialized) { + Log.e(TAG, "Sprite not initialized."); + return; + } + + // Select the shader. + if (mShader == null) { + LG.d("The shader does not exist."); + return; + } + mShader.bind(); + + // Set shader and geometry. + mVertices.position(0); + mTexCoords.position(0); + mShader.setVertices(mVertices); + mShader.setTexCoords(mTexCoords); + Matrix.translateM(this.transform, 0, transform, 0, u, v, 0); + Matrix.rotateM(this.transform, 0, rotationAngleDegrees, 0, 0, 1.0f); + if (scale != 1.0f) { + Matrix.scaleM(this.transform, 0, scale, scale, scale); + } + mShader.setTransform(this.transform); + + if (mTextures.isEmpty()) { + LG.d("Error : no textures defined for Sprite"); + return; + } + mTextures.get(0).getTexture().bind(mShader); + + // Draw the message. + mIndices.position(0); + GLES20.glDrawElements(GLES20.GL_TRIANGLES, numIndices, GLES20.GL_UNSIGNED_SHORT, mIndices); + } + + /** + * Draw the sprite in 2-D with u and v as the center of the sprite. The + * transform parameter should be an orthographic transform with 1:1 pixel + * units. + * + * @param transform 4x4 transform matrix + * @param u center U coordinate of the sprite. + * @param v center V coordinate of the sprite. + * @param rotationAngleDegrees the rotation angle for the sprite to be drawn. + * The sprite will be rotated about its center. + * @throws OpenGLException + */ + public void drawRotatedCentered( + float[] transform, float u, float v, float rotationAngleDegrees) + throws OpenGLException { + + if (!initialized) { + Log.e(TAG, "Sprite not initialized."); + return; + } + + // Select the shader. + if (mShader == null) { + LG.d("The shader does not exist."); + return; + } + mShader.bind(); + + // Set shader and geometry. + mVertices.position(0); + mTexCoords.position(0); + mShader.setVertices(mVertices); + mShader.setTexCoords(mTexCoords); + Matrix.translateM(this.transform, 0, transform, 0, u + halfX, v + halfY, 0); + Matrix.rotateM(this.transform, 0, rotationAngleDegrees, 0, 0, 1.0f); + mShader.setTransform(this.transform); + + if (mTextures.isEmpty()) { + LG.d("Error : no textures defined for Sprite"); + return; + } + mTextures.get(0).getTexture().bind(mShader); + + // Draw the message. + mIndices.position(0); + GLES20.glDrawElements(GLES20.GL_TRIANGLES, numIndices, GLES20.GL_UNSIGNED_SHORT, mIndices); + } + + /** + * Draw given a global transform without first binding to a shader. Use this + * when drawing multiple copies of the same sprite. + * + * @param globalTransform - 4x4 transform matrix for the sprite. + * @throws OpenGLException + */ + public void drawNoBinding(float[] globalTransform) throws OpenGLException { + + if (!initialized) { + Log.e(TAG, "Sprite not initialized."); + return; + } + + // Set geometry. + mVertices.position(0); + mTexCoords.position(0); + mShader.setVertices(mVertices); + mShader.setTexCoords(mTexCoords); + Matrix.multiplyMM(transform, 0, globalTransform, 0, objectTransform, 0); + mShader.setTransform(transform); + + if (mTextures.isEmpty()) { + LG.d("Error : no textures defined for Sprite2d"); + return; + } + mTextures.get(0).getTexture().bind(mShader); + + // Draw the sprite. + mIndices.position(0); + GLES20.glDrawElements(GLES20.GL_TRIANGLES, numIndices, GLES20.GL_UNSIGNED_SHORT, mIndices); + } + + protected static float computeScale( + Context context, int drawable, float pctWidth, int surfaceWidth) { + BitmapFactory.Options opts = new BitmapFactory.Options(); + opts.inJustDecodeBounds = true; + Bitmap bitmap = BitmapFactory.decodeResource( + context.getResources(), drawable, opts); + + float scale = pctWidth * surfaceWidth / opts.outWidth; + // If the scale is close to 1:1, use 1:1. + float tolerance = 0.1f; + return (scale > 1.0f - tolerance && scale < 1.0f + tolerance) ? 1.0f + : scale; + } + + protected static float computeScale( + Bitmap bitmap, float pctWidth, int surfaceWidth) { + float scale = pctWidth * surfaceWidth / bitmap.getWidth(); + + // If the scale is close to 1:1, use 1:1. + float tolerance = 0.1f; + return (scale > 1.0f - tolerance && scale < 1.0f + tolerance) ? 1.0f + : scale; + } + + /** + * Factory to create a sprite given a drawable sized to be resolution + * independent. It creates the sprite scaled to be a percentage of the OpenGL + * surface width. + * + * @param context - Android context. + * @param drawable - Android drawable. + * @param pctWidth - width of the sprite as a percentage of the surface width. + * @param surfaceWidth - width of the OpenGL surface. + * @param depth - depth that the sprite should be displayed (typically -1). + * @return the newly initialized Sprite object. + */ + public static Sprite create2dScaled(Context context, int drawable, + float pctWidth, int surfaceWidth, int depth) { + float scale = computeScale(context, drawable, pctWidth, surfaceWidth); + Sprite sprite = new Sprite(); + sprite.init2D(context, drawable, -1.0f, scale); + return sprite; + } + +} diff --git a/paperscope/photosphere/lightcycle/opengl/TextureLoaderManager.java b/paperscope/photosphere/lightcycle/opengl/TextureLoaderManager.java new file mode 100644 index 00000000..c0b8e168 --- /dev/null +++ b/paperscope/photosphere/lightcycle/opengl/TextureLoaderManager.java @@ -0,0 +1,96 @@ +// Copyright 2013 Google Inc. All Rights Reserved. + +package com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.opengl; + +import android.graphics.Bitmap; +import com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.util.Callback; +import com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.viewer.Tile; +import com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.viewer.TileProvider; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Semaphore; + +/** + * Creates {@link DelayedTextureLoader} instances and manages the loading + * semaphore. + * + * @author haeberling@google.com (Sascha Haeberling) + */ +public class TextureLoaderManager { + /** Controls how many bitmap creations are happening at once. */ + private Semaphore bitmapLoadingSemaphore; + + private final TileProvider tileProvider; + private final List loaders = new ArrayList(); + + /** + * This instance is given to all texture loaders. It's called when any of them loaded a texture + * bitmap or when textures are performing a fade-in, and thus require a re-render of the scene. + */ + private final Callback triggerRenderCallbackInternal; + + /** + * This is set from externally. If set, it is called whenever one of the loaders has finished + * loading a texture bitmap or needs rendering to do an ongoing texture fade-in. + */ + private Callback triggerRenderCallback; + + /** + * Initializes a new manager using the given tile provider. + */ + public TextureLoaderManager(TileProvider tileProvider) { + this.tileProvider = tileProvider; + triggerRenderCallbackInternal = + new Callback() { + @Override + public void onCallback(Void response) { + if (triggerRenderCallback != null) { + triggerRenderCallback.onCallback(response); + } + } + }; + reset(); + } + + /** + * Resets this factory by canceling all pending texture loading threads. + */ + public void reset() { + for (DelayedTextureLoader loader : loaders) { + loader.cancel(); + } + loaders.clear(); + bitmapLoadingSemaphore = new Semaphore(2); + } + + /** + * Sets the callback that is notified when the scene needs to be re-rendered. + * E.g. if an any texture has finished loading a texture bitmap. Notifying the + * callback should then be used to trigger a re-rendering of the scene in + * order to display the newly loaded texture. + * + * @param callback the callback to set. + */ + public void setTriggerRenderCallback(Callback callback) { + triggerRenderCallback = callback; + } + + /** + * Returns a delayed texture loader that will return null as a texture until + * the texture is loaded (which happens asynchronously). + * + * @return A new delayed texture loader. + */ + public TextureProvider getDelayedLoader(final int x, final int y) { + DelayedTextureLoader loader = + new DelayedTextureLoader(this.bitmapLoadingSemaphore, triggerRenderCallbackInternal) { + @Override + protected Bitmap loadBitmap() { + Tile tile = tileProvider.getTile(x, tileProvider.getTileCountY() - y - 1); + return tile.bitmap; + } + }; + loaders.add(loader); + return loader; + } +} diff --git a/paperscope/photosphere/lightcycle/opengl/TextureProvider.java b/paperscope/photosphere/lightcycle/opengl/TextureProvider.java new file mode 100644 index 00000000..c54ec946 --- /dev/null +++ b/paperscope/photosphere/lightcycle/opengl/TextureProvider.java @@ -0,0 +1,39 @@ +// Copyright 2013 Google Inc. All Rights Reserved. + +package com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.opengl; + +/** + * Provides and recycles a texture. + * + * @author haeberling@google.com (Sascha Haeberling) + */ +public interface TextureProvider { + /** + * Provides a texture or returns null if a texture cannot be provided at this + * point. + * + * @return A texture or null. + */ + public GLTexture getTexture(); + + /** + * Recycles the texture, if present. + */ + public void recycle(); + + /** + * This should be called from the GL thread. if a bitmap is ready this creates + * a valid texture from it. Otherwise this is a no-op. + *

+ * It does NOT trigger the loading of the texture as {@link #getTexture()} + * does. Call this method in the rendering loop to make sure textures are + * created when the bitmap becomes available. + */ + public void createTexture(); + + /** + * Returns the alpha that should be used for the texture. Implementations + * might animate the alpha over time when new textures have been loaded. + */ + public float getAlphaForRendering(); +} diff --git a/paperscope/photosphere/lightcycle/opengl/Vertex.java b/paperscope/photosphere/lightcycle/opengl/Vertex.java new file mode 100644 index 00000000..75fa3eb3 --- /dev/null +++ b/paperscope/photosphere/lightcycle/opengl/Vertex.java @@ -0,0 +1,34 @@ +// Copyright 2012 Google Inc. All Rights Reserved. + +package com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.opengl; + +import java.nio.FloatBuffer; + +/** + * Simple vertex data class. + * + * @author haeberling@google.com (Sascha Haeberling) + */ +public class Vertex { + public final float x; + public final float y; + public final float z; + + public Vertex(float x, float y, float z) { + this.x = x; + this.y = y; + this.z = z; + } + + /** + * Add this vertex to the specified vertex buffer. + * + * @param buffer the buffer to add this vertex to. + * @param offset the offset. + */ + public void addToBuffer(FloatBuffer buffer, int offset) { + buffer.put(offset, x); + buffer.put(offset + 1, y); + buffer.put(offset + 2, z); + } +} diff --git a/paperscope/photosphere/lightcycle/shaders/PanoSphereShader.java b/paperscope/photosphere/lightcycle/shaders/PanoSphereShader.java new file mode 100644 index 00000000..f1af674e --- /dev/null +++ b/paperscope/photosphere/lightcycle/shaders/PanoSphereShader.java @@ -0,0 +1,50 @@ +// Copyright 2011 Google Inc. All Rights Reserved. + +package com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.shaders; + +import com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.opengl.OpenGLException; +import com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.opengl.Shader; + +/** + * @author settinger@google.com (Scott Ettinger) + * + * Shader used to render the panorama sphere with a fixed transparency. It also + * renders any area within the texture with zero intensity as fully + * transparent. + * + */ +public class PanoSphereShader extends Shader { + + public PanoSphereShader() throws OpenGLException { + mProgram = createProgram(vertexShader, fragmentShader); + + mVertexIndex = getAttribute(mProgram, "aPosition"); + mTextureCoordIndex = getAttribute(mProgram, "aTextureCoord"); + mTransformIndex = getUniform(mProgram, "uMvpMatrix"); + //mSamplerIndex = getUniform(mProgram, "sTexture"); + } + + private final String vertexShader = + "uniform mat4 uMvpMatrix; \n" + + "attribute vec4 aPosition; \n" + + "attribute vec2 aTextureCoord; \n" + + "varying vec2 vTexCoord; \n" + + "void main() \n" + + "{ \n" + + " gl_Position = uMvpMatrix * aPosition; \n" + + " vTexCoord = aTextureCoord; \n" + + "} \n"; + + private String fragmentShader = + "precision mediump float; \n" + + "varying vec2 vTexCoord; \n" + + "uniform sampler2D sTexture; \n" + + "void main() \n" + + "{ \n" + + " vec4 texcolor; \n" + + " texcolor = texture2D( sTexture, vTexCoord ); \n" + + " texcolor.a = 0.85; \n" + + " if (texcolor.r < .0001) texcolor.a = 0.0; \n" + + " gl_FragColor = texcolor; \n" + + "} \n"; +} diff --git a/paperscope/photosphere/lightcycle/shaders/ScaledTransparencyShader.java b/paperscope/photosphere/lightcycle/shaders/ScaledTransparencyShader.java new file mode 100644 index 00000000..f3ca440d --- /dev/null +++ b/paperscope/photosphere/lightcycle/shaders/ScaledTransparencyShader.java @@ -0,0 +1,61 @@ +// Copyright 2012 Google Inc. All Rights Reserved. + +package com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.shaders; + +import android.opengl.GLES20; +import com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.opengl.OpenGLException; +import com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.opengl.Shader; + +/** + * Shader with variable transparency. This shader multiplies the existing + * alpha value in the texture by a variable alpha value. This is useful for + * compositing textures with transparency already baked into the texture. + * + * @author settinger@google.com (Scott Ettinger) + */ +public class ScaledTransparencyShader extends Shader { + + private int alphaFactorIndex = 0; + + public ScaledTransparencyShader() throws OpenGLException { + mProgram = createProgram(vertexShader, fragmentShader); + mVertexIndex = getAttribute(mProgram, "aPosition"); + mTextureCoordIndex = getAttribute(mProgram, "aTextureCoord"); + mTransformIndex = getUniform(mProgram, "uMvpMatrix"); + alphaFactorIndex = getUniform(mProgram, "uAlphaFactor"); + + // Set default values. + this.bind(); + GLES20.glUniform1f(alphaFactorIndex, 1f); + } + + // ----------------------- Parameter set methods --------------------------- + + public void setAlpha(float offset) { + GLES20.glUniform1f(alphaFactorIndex, offset); + } + + // ---------------------------- Shader code -------------------------------- + + private final String vertexShader = + "uniform mat4 uMvpMatrix; \n" + + "attribute vec4 aPosition; \n" + + "attribute vec2 aTextureCoord; \n" + + "varying vec2 vTexCoord; \n" + + "void main() \n" + + "{ \n" + + " gl_Position = uMvpMatrix * aPosition; \n" + + " vTexCoord = aTextureCoord; \n" + + "} \n"; + + private String fragmentShader = + "precision mediump float; \n" + + "uniform float uAlphaFactor; \n" + + "varying vec2 vTexCoord; \n" + + "uniform sampler2D sTexture; \n" + + "void main() \n" + + "{ \n" + + " gl_FragColor = texture2D( sTexture, vTexCoord); \n" + + " gl_FragColor.a = gl_FragColor.a * uAlphaFactor; \n" + + "} \n"; +} diff --git a/paperscope/photosphere/lightcycle/shaders/TransparencyShader.java b/paperscope/photosphere/lightcycle/shaders/TransparencyShader.java new file mode 100644 index 00000000..68e12629 --- /dev/null +++ b/paperscope/photosphere/lightcycle/shaders/TransparencyShader.java @@ -0,0 +1,63 @@ +// Copyright 2011 Google Inc. All Rights Reserved. + +package com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.shaders; + +import android.opengl.GLES20; +import com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.opengl.OpenGLException; +import com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.opengl.Shader; + +/** + * @author settinger@google.com (Scott Ettinger) + * + * Shader with variable transparency. This shader simply sets the alpha value + * for all fragments to a variable value. + * + */ +public class TransparencyShader extends Shader { + + private int alphaFactorIndex = 0; + + public TransparencyShader() throws OpenGLException { + mProgram = createProgram(vertexShader, fragmentShader); + mVertexIndex = getAttribute(mProgram, "aPosition"); + mTextureCoordIndex = getAttribute(mProgram, "aTextureCoord"); + mTransformIndex = getUniform(mProgram, "uMvpMatrix"); + alphaFactorIndex = getUniform(mProgram, "uAlphaFactor"); + + // Set default values. + this.bind(); + GLES20.glUniform1f(alphaFactorIndex, 0.9f); + } + + //----------------------- Parameter set methods --------------------------- + + public void setAlpha(float offset) { + GLES20.glUniform1f(alphaFactorIndex, offset); + } + + // ---------------------------- Shader code -------------------------------- + + private final String vertexShader = + "uniform mat4 uMvpMatrix; \n" + + "attribute vec4 aPosition; \n" + + "attribute vec2 aTextureCoord; \n" + + "varying vec2 vTexCoord; \n" + + "void main() \n" + + "{ \n" + + " gl_Position = uMvpMatrix * aPosition; \n" + + " vTexCoord = aTextureCoord; \n" + + "} \n"; + + private String fragmentShader = + "precision highp float; \n" + + "uniform float uAlphaFactor; \n" + + "varying vec2 vTexCoord; \n" + + "uniform sampler2D sTexture; \n" + + "void main() \n" + + "{ \n" + + " vec4 texcolor; \n" + + " texcolor = texture2D( sTexture, vTexCoord ); \n" + + " texcolor.a = uAlphaFactor; \n" + + " gl_FragColor = texcolor; \n" + + "} \n"; +} diff --git a/paperscope/photosphere/lightcycle/util/Callback.java b/paperscope/photosphere/lightcycle/util/Callback.java new file mode 100644 index 00000000..e040d98a --- /dev/null +++ b/paperscope/photosphere/lightcycle/util/Callback.java @@ -0,0 +1,18 @@ +// Copyright 2012 Google Inc. All Rights Reserved. + +package com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.util; + +/** + * A simple general purpose callback interface. + * + * @author haeberling@google.com (Sascha Haeberling) + * + * @param The type of the callback response instance. + */ +public interface Callback { + + /** + * The callback method. + */ + public void onCallback(T response); +} diff --git a/paperscope/photosphere/lightcycle/util/DepthMap.java b/paperscope/photosphere/lightcycle/util/DepthMap.java new file mode 100644 index 00000000..1ed7e249 --- /dev/null +++ b/paperscope/photosphere/lightcycle/util/DepthMap.java @@ -0,0 +1,181 @@ +package com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.util; + +import android.util.Base64; +import android.util.Log; +import java.io.ByteArrayInputStream; +import java.io.IOException; + +/** + * Contains a low-res depth map for pixels of a streetview panorama. + * + * This code is a self-contained, simplified version of + * java/com/google/android/apps/gmm/streetview/internal/DepthMap.java. + * + * Contrary to the original code, all the per-pixel depth data is computed at startup rather than + * on-demand, as we expect that data to be queried often by clients. + */ +public class DepthMap { + private static final String TAG = "ps.DepthMap"; + + // Depth value that is used when we don't have depth information for some pixel. + // TODO(dcoz): refine this. + public static final float INFINITE_DEPTH_METERS = 100.0f; + + // Fields below correspond to data encoded in the encoded depthmap string coming from + // StreetView servers, ie, a set of plane equations, and a plane index for each pixel of the + // depthmap. + private int numPlanes; + private int mapWidth; + private int mapHeight; + private byte[] planeIndices; + private Plane[] planes; + + // Contains the decoded depth for each cell in the depthmap, derived from the plane data above. + // This format is easier to use for clients. + private float[] depthMap; + + // A simple plane class, (x, y, z) points of the plane satisfy x * nx + y * ny + z * nz = d. + private static class Plane { + public final float nx; + public final float ny; + public final float nz; + public final float d; + + public Plane(float nx, float ny, float nz, float d) { + this.nx = nx; + this.ny = ny; + this.nz = nz; + this.d = d; + } + + // Get distance between this plane and the origin, along the (x, y, z) ray. + public float getDepthForRay(float x, float y, float z) { + return d / (x * nx + y * ny + z * nz); + } + } + + // Instantiate depthmap from an uncompressed depthmap string typically fetched from the + // StreetView server with the &dmz=1 parameter in the URL. + public DepthMap(String uncompressedDepthMap) throws IOException { + if (!decodeUncompressedDepthMap(uncompressedDepthMap)) { + throw new IOException("Error decoding provided depth map"); + } + + computePerPixelDepthMap(); + } + + /** + * Get depth in meters for point located at (x, y) in the depth map image. + * (x, y) are [0, 1] normalized coordinates. + */ + public float getDepth(double x, double y) { + int cellX = (int) (x * mapWidth); + int cellY = (int) (y * mapHeight); + + cellX = Math.max(0, Math.min(mapWidth - 1, cellX)); + cellY = Math.max(0, Math.min(mapHeight - 1, cellY)); + + // TODO(dcoz): add interpolation? + return getDepthAtCell(cellX, cellY); + } + + public float getDepthAtCell(int x, int y) { + return depthMap[y * mapWidth + x]; + } + + private void setDepth(int x, int y, float depth) { + depthMap[y * mapWidth + x] = depth; + } + + // Converts plane-index-per-pixel data to a more friendly depth-per-pixel data. + private void computePerPixelDepthMap() { + assert planeIndices != null; + depthMap = new float [mapWidth * mapHeight]; + + for (int y = 0; y < mapHeight; ++y) { + for (int x = 0; x < mapWidth; ++x) { + int planeIndex = planeIndices[y * mapWidth + x]; + + if (planeIndex <= 0) { + setDepth(x, y, INFINITE_DEPTH_METERS); + } else { + assert planeIndex < numPlanes; + Plane plane = planes[planeIndex]; + + // Convert (x, y) coordinates into lat, lng coordinates on a sphere. + double lng = getLngFromX(x * 1.0 / mapWidth); + double lat = getLatFromY(y * 1.0 / mapHeight); + + // Compute depth along the ray that points to this (lat, lng) point. + float dx = (float) (Math.sin(lat) * Math.sin(lng)); + float dy = (float) (Math.sin(lat) * Math.cos(lng)); + float dz = (float) -Math.cos(lat); + float depth = Math.abs(plane.getDepthForRay(dx, dy, dz)); + + setDepth(x, y, depth); + } + } + } + + } + + // Decodes the raw uncompressed depthmap string. + private boolean decodeUncompressedDepthMap(String uncompressedDepthMap) { + // First, base64 decode the string. + final byte[] base64Decoded = Base64.decode(uncompressedDepthMap, Base64.URL_SAFE); + + // Read this data with Little Endian input stream. + LEDataInputStream is = new LEDataInputStream(new ByteArrayInputStream(base64Decoded)); + + try { + // Read header and depthmap size. + int headerSize = is.readUnsignedByte(); + assert headerSize == 8; + numPlanes = is.readUnsignedShort(); + mapWidth = is.readUnsignedShort(); + mapHeight = is.readUnsignedShort(); + + int planeOffset = is.readUnsignedByte(); + assert planeOffset == 8; + + planeIndices = new byte[mapWidth * mapHeight]; + is.readFully(planeIndices); + + planes = new Plane[numPlanes]; + float nx; + float ny; + float nz; + float d; + + // Read all plane equations. + for (int i = 0; i < numPlanes; ++i) { + nx = is.readFloat(); + ny = is.readFloat(); + nz = is.readFloat(); + d = is.readFloat(); + planes[i] = new Plane(nx, ny, nz, d); + } + } catch (Exception e) { + Log.w(TAG, "Error reading depth map", e); + return false; + } finally { + try { + is.close(); + } catch (IOException e) { + Log.w(TAG, "error closing IS ", e); + return false; + } + } + return true; + } + + // Utility function to convert from normalized [0, 1] x [0, 1] image rectangular + // coordinates to [0, 2 PI] x [0, PI] lat, lng coordinates on a sphere. + public static double getLngFromX(double x) { + return x * Math.PI * 2; + } + + public static double getLatFromY(double y) { + return y * Math.PI; + } +} diff --git a/paperscope/photosphere/lightcycle/util/InputStreamFactory.java b/paperscope/photosphere/lightcycle/util/InputStreamFactory.java new file mode 100644 index 00000000..9c3d3232 --- /dev/null +++ b/paperscope/photosphere/lightcycle/util/InputStreamFactory.java @@ -0,0 +1,19 @@ +// Copyright 2012 Google Inc. All Rights Reserved. + +package com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.util; + +import java.io.InputStream; + +/** + * Creates input streams. + * + * TODO(haeberling): Move to common package when possible. + * + * @author haeberling@google.com (Sascha Haeberling) + */ +public interface InputStreamFactory { + /** + * @return The newly created {@link InputStream}. + */ + public InputStream create(); +} diff --git a/paperscope/photosphere/lightcycle/util/LEDataInputStream.java b/paperscope/photosphere/lightcycle/util/LEDataInputStream.java new file mode 100644 index 00000000..cbc96654 --- /dev/null +++ b/paperscope/photosphere/lightcycle/util/LEDataInputStream.java @@ -0,0 +1,102 @@ +package com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.util; + +import java.io.DataInput; +import java.io.DataInputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * A little-endian DataInputStream. + * + * NOTE(dcoz): copied from + * java/com/google/android/apps/gmm/streetview/internal/LEDataInputStream.java + * + * @author law@google.com (Kevin Law) + */ +class LEDataInputStream extends FilterInputStream implements DataInput { + /** This underlying DIS implements the endian-agnostic methods. */ + DataInputStream dataInputStream; + + public LEDataInputStream(InputStream inputStream) { + super(inputStream); + dataInputStream = new DataInputStream(inputStream); + } + + @Override + public boolean readBoolean() throws IOException { + return read() != 0; + } + + @Override + public byte readByte() throws IOException { + return (byte) read(); + } + + @Override + public char readChar() { + throw new UnsupportedOperationException(); + } + + @Override + public double readDouble() { + throw new UnsupportedOperationException(); + } + + @Override + public float readFloat() throws IOException { + return Float.intBitsToFloat(readInt()); + } + + @Override + public void readFully(byte[] buffer) throws IOException { + dataInputStream.readFully(buffer); + } + + @Override + public void readFully(byte[] buffer, int offset, int length) + throws IOException { + dataInputStream.readFully(buffer, offset, length); + } + + @Override + public int readInt() throws IOException { + return readUnsignedByte() | (readUnsignedByte() << 8) | + (readUnsignedByte() << 16) | (readUnsignedByte() << 24); + } + + @Override + public String readLine() { + throw new UnsupportedOperationException(); + } + + @Override + public long readLong() { + throw new UnsupportedOperationException(); + } + + @Override + public short readShort() { + throw new UnsupportedOperationException(); + } + + @Override + public String readUTF() { + throw new UnsupportedOperationException(); + } + + @Override + public int readUnsignedByte() throws IOException { + return read() & 0xff; + } + + @Override + public int readUnsignedShort() throws IOException { + return readUnsignedByte() | (readUnsignedByte() << 8); + } + + @Override + public int skipBytes(int n) { + throw new UnsupportedOperationException(); + } +} diff --git a/paperscope/photosphere/lightcycle/util/LG.java b/paperscope/photosphere/lightcycle/util/LG.java new file mode 100644 index 00000000..8fcd9973 --- /dev/null +++ b/paperscope/photosphere/lightcycle/util/LG.java @@ -0,0 +1,44 @@ +// Copyright 2011 Google Inc. All Rights Reserved. + +package com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.util; + +import android.util.Log; + +/** + * @author settinger@google.com (Scott Ettinger) + * + * Logger class that outputs the class and line number of the caller. The + * output can be slow, so it should be used in non-performance critical paths + * or for debug only. + */ +public class LG { + + public static final boolean DEBUG_LOGGING_ENABLED = false; + + public static final boolean VERBOSE = true; + public static final String TAG = "LightCycle-debug"; + + // Output class and line number to the log if Verbose is set to true. + public static void d(String message) { + if (!DEBUG_LOGGING_ENABLED) { + return; + } + if (VERBOSE) { + String className = + Thread.currentThread().getStackTrace()[3].getClassName(); + String shortClass = + className.substring(className.lastIndexOf(".") + 1); + String method = + Thread.currentThread().getStackTrace()[3].getMethodName(); + int line = + Thread.currentThread().getStackTrace()[3].getLineNumber(); + + Log.v(TAG, shortClass + "." + method + "():" + line + " : " + + message); + } else { + Log.v(TAG, message); + } + } + +} + diff --git a/paperscope/photosphere/lightcycle/util/PanoMetadata.java b/paperscope/photosphere/lightcycle/util/PanoMetadata.java new file mode 100644 index 00000000..1eeee4db --- /dev/null +++ b/paperscope/photosphere/lightcycle/util/PanoMetadata.java @@ -0,0 +1,369 @@ +// Copyright 2012 Google Inc. All Rights Reserved. + +package com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.util; + +import android.graphics.BitmapFactory; +import android.util.Log; +import com.adobe.xmp.XMPException; +import com.adobe.xmp.XMPMeta; +import com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.xmp.PanoConstants; +import com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.xmp.XmpUtil; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.Calendar; +import org.jspecify.annotations.Nullable; + +/** + * Reads the meta data from a panorama file. + * + * TODO(bwuest): This should not be forked from lightcycle, but rather shared as a library, can + * be included through LightCycleCoreLib. + * @author haeberling@google.com (Sascha Haeberling) + */ +public class PanoMetadata { + private static final String TAG = "PanoMetadata"; + + /** Width of the image in pixels. */ + public final int imageWidth; + /** Height of the image in pixels. */ + public final int imageHeight; + + /** Time the first photo of the pano series was taken. */ + public final Calendar firstPhotoTime; + /** Time the last photo of the pano series was taken. */ + public final Calendar lastPhotoTime; + /** The number of photos to produce the panorama. */ + public final int sourcePhotosCount; + /** The projection type needed to display the panorama correctly. */ + public final String projectionType; + /** Whether a panorama viewer should be used. */ + public final boolean usePanoViewer; + + /** The width of the cropped area in pixels. */ + public final int croppedAreaWidth; + /** The height of the cropped area in pixels. */ + public final int croppedAreaHeight; + /** The width of the original full 360x180 degree pano in pixels. */ + public final int fullPanoWidth; + /** The height of the original full 360x180 degree pano in pixels. */ + public final int fullPanoHeight; + /** + * The offset of the cropped area from the left edge of the fullsize pano in + * pixels. + */ + public final int croppedAreaLeft; + /** + * The offset of the cropped area from the top edge of the fullsize pano in + * pixels. + */ + public final int croppedAreaTop; + + /** + * Compass heading, measured in degrees, for the center of the image. Value must + * be >= 0 and < 360. + *

+ * The order in which heading, pitch and roll are applied is important. See + * https://developers.google.com/photo-sphere/metadata/ for details. + */ + public float poseHeadingDegrees; + + /** + * Pitch, measured in degrees, for the center of the image. Value must be >= + * -90 and <= 90. + *

+ * The order in which heading, pitch and roll are applied is important. See + * https://developers.google.com/photo-sphere/metadata/ for details. + */ + public final float posePitchDegrees; + + /** + * Roll, measured in degrees, of the image where level with the horizon is 0. + * Value must be > -180 and <= 180. + *

+ * The order in which heading, pitch and roll are applied is important. See + * https://developers.google.com/photo-sphere/metadata/ for details. + */ + public final float poseRollDegrees; + + /** + * Whether the meta-data was synthetically generated by a 2:1 file without + * meta data. + */ + public final boolean synthetic; + + /** + * Depthmap associated to the panorama. This field is optional and can be null. + */ + public DepthMap depthMap; + + /** + * Creating an instance will cause the specified file's meta data to be + * parsed. + */ + private PanoMetadata(int imageWidth, int imageHeight, Calendar firstPhotoTime, + Calendar lastPhotoTime, int sourcePhotosCount, String projectionType, + boolean usePanoViewer, int croppedAreaWidth, int croppedAreaHeight, + int fullPanoWidth, int fullPanoHeight, int croppedAreaLeft, + int croppedAreaTop, float poseHeadingDegrees, float posePitchDegrees, + float poseRollDegrees, boolean synthetic) { + this.imageWidth = imageWidth; + this.imageHeight = imageHeight; + this.firstPhotoTime = firstPhotoTime; + this.lastPhotoTime = lastPhotoTime; + this.sourcePhotosCount = sourcePhotosCount; + this.projectionType = projectionType; + this.usePanoViewer = usePanoViewer; + this.croppedAreaWidth = croppedAreaWidth; + this.croppedAreaHeight = croppedAreaHeight; + this.fullPanoWidth = fullPanoWidth; + this.fullPanoHeight = fullPanoHeight; + this.croppedAreaLeft = croppedAreaLeft; + this.croppedAreaTop = croppedAreaTop; + this.poseHeadingDegrees = poseHeadingDegrees; + this.posePitchDegrees = posePitchDegrees; + this.poseRollDegrees = poseRollDegrees; + this.synthetic = synthetic; + } + + /** + * Create a new instance of PanoMetadata. This is used for example where the source + * will be a panorama served from cityblock and we have not yet loaded any tile images + * for it. The width and height should be the size at the desired zoom level that is + * being fetched, not necessarily the full size of the available image. + * + * If this constructor is used directly no checks on aspect ratio etc. are performed + * and these must be done by the caller. + * + * @param imageWidth The width of the panorama at the desired zoom level in pixels + * @param imageHeight The height of the panorama at the desired zoom level in pixels + */ + public PanoMetadata(int imageWidth, int imageHeight) { + this.imageWidth = imageWidth; + this.imageHeight = imageHeight; + firstPhotoTime = Calendar.getInstance(); + lastPhotoTime = Calendar.getInstance(); + sourcePhotosCount = -1; + projectionType = PanoConstants.PROPERTY_VALUE_EQUIRECTANGULAR; + usePanoViewer = true; + croppedAreaWidth = imageWidth; + croppedAreaHeight = imageHeight; + croppedAreaLeft = 0; + croppedAreaTop = 0; + fullPanoWidth = imageWidth; + fullPanoHeight = imageHeight; + poseHeadingDegrees = 0; + posePitchDegrees = 0; + poseRollDegrees = 0; + synthetic = true; + } + + /** + * Sets the depthmap associated to the panorama. + */ + public void setDepthMap(DepthMap depthMap) { + this.depthMap = depthMap; + } + + /** + * Sets pose heading offset associated to the panorama. + */ + public void setPoseHeadingDegrees(float poseHeadingDegrees) { + this.poseHeadingDegrees = poseHeadingDegrees; + } + + /** + * Parses the pano-specific meta data from the given file and returns an + * instance of {@link PanoMetadata} containing the parsed values. + * + * @param filename the name of the file to extract the XMP meta data from. + * @return The meta data extracted from the file or null, of no XMP meta data + * was found, it failed to parse or is invalid. + */ + public static PanoMetadata parse(final String filename) { + return parse( + new InputStreamFactory() { + + @Override + public @Nullable InputStream create() { + try { + return new FileInputStream(filename); + } catch (FileNotFoundException e) { + Log.e(TAG, "Could not read file: " + filename, e); + return null; + } + } + }); + } + + /** See {@link #parse(String)}, but use an {@link InputStream} instead. */ + public static @Nullable PanoMetadata parse(InputStreamFactory isf) { + InputStream is = isf.create(); + if (is == null) { + return null; + } + + Calendar firstPhotoTime = null; + Calendar lastPhotoTime = null; + int sourcePhotosCount = 0; + String projectionType = null; + boolean usePanoViewer = false; + int croppedAreaWidth = 0; + int croppedAreaHeight = 0; + int fullPanoWidth = 0; + int fullPanoHeight = 0; + int croppedAreaLeft = 0; + int croppedAreaTop = 0; + float poseHeadingDegrees = 0; + float posePitchDegrees = 0; + float poseRollDegrees = 0; + boolean synthetic = false; + + boolean validMetaData = false; + XMPMeta xmp = XmpUtil.extractXmpMeta(is); + + if (xmp != null) { + try { + firstPhotoTime = getDate(xmp, PanoConstants.FIRST_PHOTO_DATE); + lastPhotoTime = getDate(xmp, PanoConstants.LAST_PHOTO_DATE); + sourcePhotosCount = getInt(xmp, PanoConstants.SOURCE_PHOTOS_COUNT); + projectionType = getString(xmp, PanoConstants.PROPERTY_PROJECTION_TYPE); + usePanoViewer = + getBoolean(xmp, PanoConstants.PROPERTY_USE_PANORAMA_VIEWER); + croppedAreaWidth = + getInt(xmp, PanoConstants.CROPPED_AREA_IMAGE_WIDTH_PIXELS); + croppedAreaHeight = + getInt(xmp, PanoConstants.CROPPED_AREA_IMAGE_HEIGHT_PIXELS); + fullPanoWidth = + getInt(xmp, PanoConstants.CROPPED_AREA_FULL_PANO_WIDTH_PIXELS); + fullPanoHeight = + getInt(xmp, PanoConstants.CROPPED_AREA_FULL_PANO_HEIGHT_PIXELS); + croppedAreaLeft = getInt(xmp, PanoConstants.CROPPED_AREA_LEFT); + croppedAreaTop = getInt(xmp, PanoConstants.CROPPED_AREA_TOP); + poseHeadingDegrees = (float) getDouble(xmp, PanoConstants.POSE_HEADING_DEGREES); + posePitchDegrees = (float) getDouble(xmp, PanoConstants.POSE_PITCH_DEGREES); + poseRollDegrees = (float) getDouble(xmp, PanoConstants.POSE_ROLL_DEGREES); + synthetic = false; + + validMetaData = croppedAreaWidth > 0 && croppedAreaHeight > 0 + && fullPanoWidth > 0 && fullPanoHeight > 0; + } catch (XMPException ex) { + // Use defaults if XMP parsing fails. + } + } + + // Get the actual image width and height. + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + is = isf.create(); + try { + BitmapFactory.decodeStream(is, null, options); + } finally { + try { + if (is != null) { + is.close(); + } + } catch (IOException e) { + // ignore + } + } + int imageWidth = options.outWidth; + int imageHeight = options.outHeight; + boolean twoToOneRatio = 2 * imageHeight == imageWidth; + boolean fallback = false; + + // If the metadata could not be parsed correctly, but the image has a 2:1 + // ration, set defaults suggesting it's a 360x180. + if (!validMetaData) { + if (twoToOneRatio) { + // Could not parse meta data for file. Filling in 360 defaults. + fallback = true; + } else { + // Image has no metadata and no 2:1 aspect ratio. Not a pano! + return null; + } + } + + // Sanity checks: Make sure the given and actual aspect ratios are roughly + // the same. + double imageAspectRatio = imageWidth / (double) imageHeight; + double croppedMetaRatio = croppedAreaWidth / (double) croppedAreaHeight; + + if (!fallback && !isNear(imageAspectRatio, croppedMetaRatio, 0.05)) { + Log.w(TAG, + "Pano metadata does not match file dimensions. Image aspect ratio: " + + imageAspectRatio + ". Metadata aspect ratio: " + croppedMetaRatio); + return null; + } + + // Sanity check: Make sure the full pano aspect ratio is roughly 2:1. + if (!fallback && !isNear(fullPanoWidth / (double) fullPanoHeight, 2, 0.1)) { + Log.w(TAG, "Pano metadata invalid: Full pano dimension not 2:1."); + return null; + } + + if (fallback) { + // Initializes the metadata for a 2:1 360 degree pano at the specified dimensions + return new PanoMetadata(imageWidth, imageHeight); + } else { + // Initializes the metadata with the values read from XMP. + return new PanoMetadata(imageWidth, imageHeight, firstPhotoTime, + lastPhotoTime, sourcePhotosCount, projectionType, usePanoViewer, + croppedAreaWidth, croppedAreaHeight, fullPanoWidth, fullPanoHeight, + croppedAreaLeft, croppedAreaTop, poseHeadingDegrees, posePitchDegrees, poseRollDegrees, + synthetic); + } + } + + public boolean isScaled() { + return this.imageWidth != this.croppedAreaWidth + || this.imageHeight != this.croppedAreaHeight; + } + + private static double getDouble(XMPMeta xmp, String key) throws XMPException { + if (xmp.doesPropertyExist(PanoConstants.GOOGLE_PANO_NAMESPACE, key)) { + return xmp.getPropertyDouble(PanoConstants.GOOGLE_PANO_NAMESPACE, key); + } else { + return 0; + } + } + + private static int getInt(XMPMeta xmp, String key) throws XMPException { + if (xmp.doesPropertyExist(PanoConstants.GOOGLE_PANO_NAMESPACE, key)) { + return xmp.getPropertyInteger(PanoConstants.GOOGLE_PANO_NAMESPACE, key); + } else { + return 0; + } + } + + private static @Nullable String getString(XMPMeta xmp, String key) throws XMPException { + if (xmp.doesPropertyExist(PanoConstants.GOOGLE_PANO_NAMESPACE, key)) { + return xmp.getPropertyString(PanoConstants.GOOGLE_PANO_NAMESPACE, key); + } else { + return null; + } + } + + private static boolean getBoolean(XMPMeta xmp, String key) + throws XMPException { + if (xmp.doesPropertyExist(PanoConstants.GOOGLE_PANO_NAMESPACE, key)) { + return xmp.getPropertyBoolean(PanoConstants.GOOGLE_PANO_NAMESPACE, key); + } else { + return false; + } + } + + private static @Nullable Calendar getDate(XMPMeta xmp, String key) throws XMPException { + if (xmp.doesPropertyExist(PanoConstants.GOOGLE_PANO_NAMESPACE, key)) { + return xmp.getPropertyCalendar(PanoConstants.GOOGLE_PANO_NAMESPACE, key); + } else { + return null; + } + } + + private static boolean isNear(double first, double second, double tolerance) { + return Math.abs(first - second) < tolerance; + } + +} diff --git a/paperscope/photosphere/lightcycle/viewer/PanoramaImage.java b/paperscope/photosphere/lightcycle/viewer/PanoramaImage.java new file mode 100644 index 00000000..a12b4ddf --- /dev/null +++ b/paperscope/photosphere/lightcycle/viewer/PanoramaImage.java @@ -0,0 +1,169 @@ +// Copyright 2012 Google Inc. All Rights Reserved. + +package com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.viewer; + +import com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.util.DepthMap; +import com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.util.PanoMetadata; + +import java.io.IOException; + +/** + * Contains information about how to load and render a panorama image. + * + * @author haeberling@google.com (Sascha Haeberling) + */ +public class PanoramaImage { + private static final String TAG = "ps.PanoramaImage"; + + /** Provides the tiles of the image. */ + private final TileProvider tileProvider; + /** The width of the pano in radians. */ + private float panoWidthRad; + /** The height of the pano in radians. */ + private float panoHeightRad; + /** The size of the quadratic tiles in radians. */ + private float tileSizeRad; + /** The offset of the upper edge from the north pole in radians. */ + private float panoOffsetTopRad; + /** The offset of the left edge from the left-hand seam in radians. */ + private float panoOffsetLeftRad; + /** The width of the last column in radians. */ + private float lastColumnWidthRad; + /** The height of the last row in radians. */ + private float lastRowHeightRad; + + private PanoMetadata panoMetadata; + + public PanoramaImage(TileProvider tileProvider, PanoMetadata panoMetadata) { + this.tileProvider = tileProvider; + this.panoMetadata = panoMetadata; + + this.panoWidthRad = (float) ((this.panoMetadata.croppedAreaWidth + / (float) this.panoMetadata.fullPanoWidth) * Math.PI * 2); + this.panoHeightRad = (float) ((this.panoMetadata.croppedAreaHeight + / (float) this.panoMetadata.fullPanoHeight) * Math.PI); + + this.panoOffsetLeftRad = (float) ((this.panoMetadata.croppedAreaLeft + / (float) this.panoMetadata.fullPanoWidth) * Math.PI * 2); + this.panoOffsetTopRad = (float) ((this.panoMetadata.croppedAreaTop + / (float) this.panoMetadata.fullPanoHeight) * Math.PI); + } + + /** + * Builds the pano depthmap associated to the image using the given uncompressed depth map string. + */ + public void buildPanoDepthMap(String uncompressedDepthMap) throws IOException { + panoMetadata.setDepthMap(new DepthMap(uncompressedDepthMap)); + } + + public DepthMap getDepthMap() { + return panoMetadata.depthMap; + } + + public PanoMetadata getPanoMetadata() { + return panoMetadata; + } + + /** + * Compute all properties of the image. + */ + public void init() { + + // If the image was scaled without updating the metadata, the absolute + // values are wrong. This also means that the angular size per tile is not + // correct, as the tile size stays the same. If the image was scaled to half + // size, the angular size will be twice as large. + float scale = this.panoMetadata.imageWidth + / (float) this.panoMetadata.croppedAreaWidth; + + // TODO(settinger): determine how to handle the dependency of requiring + // getTile() to be called on the GL thread before calling + // getScale() or getTileSize() for early SDK versions. + + // Scale by the downsampling factor applied by the tile provider. + scale *= this.tileProvider.getScale(); + + // It doesn't matter if we use imageWidth/360, or imageHeight/180, + // the number must be the same. + this.tileSizeRad = ((float) ( + (tileProvider.getTileSize() / (float) this.panoMetadata.fullPanoWidth) + * Math.PI * 2)) / scale; + + // Compute the angular size of last column and last row tiles, which can be smaller than the + // other ones. + this.lastColumnWidthRad = panoWidthRad - tileSizeRad * (tileProvider.getTileCountX() - 1); + this.lastRowHeightRad = panoHeightRad - tileSizeRad * (tileProvider.getTileCountY() - 1); + } + + /** + * @return The tile provider for retrieving the tiles of this image. + */ + public TileProvider getTileProvider() { + return this.tileProvider; + } + + /** + * @return The width of the panorama in radians. + */ + public float getPanoWidthRad() { + return this.panoWidthRad; + } + + /** + * @return The height of the panorama in radians. + */ + public float getPanoHeightRad() { + return this.panoHeightRad; + } + + /** + * @return The size of a panorama tile in radians. + */ + public float getTileSizeRad() { + return tileSizeRad; + } + + /** + * @return The offset of the top-left corner of the cropped region in radians. + */ + public float getOffsetTopRad() { + return this.panoOffsetTopRad; + } + + /** + * @return The offset of the top-left corner of the cropped region in radians. + */ + public float getOffsetLeftRad() { + return this.panoOffsetLeftRad; + } + + /** + * @return The width of the last column in radians. + */ + public float getLastColumnWidthRad() { + return this.lastColumnWidthRad; + } + + /** + * @return The height of the last row in radians. + */ + public float getLastRowHeightRad() { + return this.lastRowHeightRad; + } + + /** + * Set the maximum texture size supported by the hardware. + * + * @param dimension the maximum texture size in pixels. + */ + public void setMaximumTextureSize(int dimension) { + this.tileProvider.setMaximumTextureSize(dimension); + } + + /** + * @return the degrees per pixel of this panorama image. + */ + public double getDegreesPerPixel() { + return Math.toDegrees((double) panoWidthRad / panoMetadata.imageWidth); + } +} diff --git a/paperscope/photosphere/lightcycle/viewer/PanoramaViewRenderer.java b/paperscope/photosphere/lightcycle/viewer/PanoramaViewRenderer.java new file mode 100644 index 00000000..680a9981 --- /dev/null +++ b/paperscope/photosphere/lightcycle/viewer/PanoramaViewRenderer.java @@ -0,0 +1,291 @@ +// Copyright 2012 Google Inc. All Rights Reserved. + +package com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.viewer; + +import android.content.Context; +import android.graphics.RectF; +import android.opengl.GLES20; +import android.opengl.GLSurfaceView; +import android.opengl.Matrix; +import android.util.Log; +import com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.Constants; +import com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.math.Vector3; +import com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.opengl.OpenGLException; +import com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.opengl.PartialSphere; +import com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.opengl.TextureLoaderManager; +import com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.shaders.TransparencyShader; +import com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.util.Callback; +import com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.util.LG; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +/** + * + * This class renders all objects in the 3D panorama viewer scene. The viewing + * angle is controlled via set functions. + * + * Note that this renderer assumes that the associated GLSurfaceView renders + * continuously, avoiding the need for explicit GLSurfaceView.requestRender() + * calls when the underlying texture changes. + * + * NOTE(dcoz): edited to remove dependency on PanoramaView and get rid of the + * cubemap dependency which we don't use. + * + * @author settinger@google.com (Scott Ettinger) + * @author haeberling@google.com (Sascha Haeberling) + */ +public class PanoramaViewRenderer implements GLSurfaceView.Renderer { + private static final String TAG = PanoramaViewRenderer.class.getSimpleName(); + + /** Auto-spin rate in degrees per frame at 90 degree FOV. */ + private static final float FINAL_PANORAMA_OPACITY = 1.0f; + + private static final float PANORAMA_FADE_IN_ALPHA = 0.05f; + + // Rendering state. + private boolean objectsInitialized = false; + private PanoramaImage image; + + // View state. + private float panoramaOpacity = 1; + + // Rendering objects. + private PartialSphere panoramaSphere; + private TextureLoaderManager textureLoaderManager; + + // Shaders. + private TransparencyShader panoSphereShader; + + // Transforms. + private float[] projection = new float[16]; + private float[] clientModelView = new float[16]; + private float[] modelView = new float[16]; + private float[] vPMatrix = new float[16]; + private float[] offsetMatrix = new float[16]; + + // Heading angle offset in degrees. + private float headingOffsetDeg; + + // Sensor data. + private Callback onInitializedCallback = null; + + private boolean initializationRequired = false; + + private RectF imageBoundsDeg = new RectF(); + + /** + * Instantiates a new renderer. Allows the cubemap resource id to be specified + * when this class is used outside Lightcycle code + * + * @param image the image that will be displayed by this renderer + * @param context the android context associated with this renderer + */ + public PanoramaViewRenderer(PanoramaImage image, Context context) { + textureLoaderManager = new TextureLoaderManager(image.getTileProvider()); + this.setPanoramaImage(image); + Matrix.setIdentityM(offsetMatrix, 0); + Matrix.setIdentityM(clientModelView, 0); + } + + /** Sets the model view matrix. */ + public void setModelViewMatrix(float[] modelViewMatrix) { + System.arraycopy(modelViewMatrix, 0, clientModelView, 0, clientModelView.length); + } + + /** Sets the projection matrix. */ + public void setProjectionMatrix(float[] projectionMatrix) { + System.arraycopy(projectionMatrix, 0, projection, 0, projection.length); + } + + /** + * Sets the heading offset in degrees, which should be taken into account when a new image is + * loaded. + */ + public void setHeadingOffset(float headingOffsetDeg) { + this.headingOffsetDeg = headingOffsetDeg; + } + + private void setPanoramaImage(PanoramaImage image) { + this.image = image; + textureLoaderManager.reset(); + + float widthDegrees = (float) Math.toDegrees(this.image.getPanoWidthRad()); + imageBoundsDeg.left = (float) (Math.toDegrees(this.image.getOffsetLeftRad() - Math.PI)); + imageBoundsDeg.right = imageBoundsDeg.left + widthDegrees; + + float heightDegrees = (float) Math.toDegrees(this.image.getPanoHeightRad()); + imageBoundsDeg.top = 90f - (float) Math.toDegrees(this.image.getOffsetTopRad()); + imageBoundsDeg.bottom = imageBoundsDeg.top - heightDegrees; + } + + /** + * Sets the callback that gets fired when the scene is initialized. + */ + public void setOnInitializedCallback(Callback callback) { + onInitializedCallback = callback; + if (objectsInitialized) { + callback.onCallback(null); + } + } + + public void destroy() { + textureLoaderManager.reset(); + } + + private void initFrame() { + GLES20.glEnable(GLES20.GL_DEPTH_TEST); + } + + /** + * Calculate the model view and projection matrices based on the current pitch + * and yaw. + */ + private synchronized void updateMatrices() { + // NOTE(dcoz): directly use client-provided model view and projection matrices. + + // Multiply client modelview matrix by the offset matrix. + Matrix.multiplyMM(modelView, 0, clientModelView, 0, offsetMatrix, 0); + Matrix.multiplyMM(vPMatrix, 0, projection, 0, modelView, 0); + } + + /** Draw all objects in the scene. */ + private void drawScene() { + updateMatrices(); + + // Clear the buffers and set the viewport. + initFrame(); + + try { + // NOTE(dcoz): we don't need a cubemap since we're displaying a full 360 panorama. + // Draw environment cube. + // mTexturedCube.draw(vPMatrix); + + // Draw the wireframe sphere. + GLES20.glLineWidth(1); + GLES20.glEnable(GLES20.GL_BLEND); + GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA); + + // Draw the panorama. + panoSphereShader.bind(); + panoSphereShader.setAlpha(panoramaOpacity); + + // NOTE(eparker): Use dummy values for lookAt and minVisibilityDot since + // they are only used for culling but isVisible() in CurvedTile.java has + // been hacked to always return true so all tiles are drawn each frame + // currently. If we decide we need culling then we'll need to compute + // lookAt minVisibilityDot properly, but the way the old code was + // computing them was incorrect. + Vector3 lookAt = new Vector3(0, 0, 1); + float minVisibilityDot = 0.0f; + panoramaSphere.draw(vPMatrix, lookAt, minVisibilityDot); + + } catch (OpenGLException e) { + e.printStackTrace(); + } + } + + /** + * Render a frame when requested by the system. + * NOTE(dcoz): this function assumes the gl viewport was set beforehand by the caller. + */ + @Override + public void onDrawFrame(GL10 gl) { + if (!objectsInitialized) { + return; + } + + if (initializationRequired) { + image.getTileProvider().setMaximumTextureSize(getMaxTextureSize()); + panoramaSphere.setImage(image, textureLoaderManager); + initializationRequired = false; + } + + // Update the Panorama fade opacity. + panoramaOpacity += (FINAL_PANORAMA_OPACITY - panoramaOpacity) * PANORAMA_FADE_IN_ALPHA; + + // Draw the scene oriented with the user's motion. + if (objectsInitialized) { + drawScene(); + } + } + + /** + * Set the new image and trigger a re-initialization of textures and geometry. + */ + public void setImage(PanoramaImage image) { + // Before loading a new image, compute a heading offset so that the new image is always centered + // where the user was looking at. + final float mHeadingOffset = headingOffsetDeg - image.getPanoMetadata().poseHeadingDegrees; + Matrix.setIdentityM(offsetMatrix, 0); + Matrix.rotateM(offsetMatrix, 0, mHeadingOffset, 0, 1, 0); + + this.image = image; + textureLoaderManager = new TextureLoaderManager(image.getTileProvider()); + initializationRequired = true; + } + + /** + * Initialize all rendering objects when the surface is initially created. + */ + @Override + public void onSurfaceChanged(GL10 gl, int width, int height) { + if (image == null) { + Log.d(TAG, "Image file not set. Cannot initialize rendering."); + return; + } + + if (!objectsInitialized) { + try { + // Initialize all rendering objects. + initRendering(); + } catch (OpenGLException e) { + e.printStackTrace(); + } + } + LG.d("Rendering init completed."); + } + + @Override + public void onSurfaceCreated(GL10 gl, EGLConfig config) {} + + /** + * Initializes rendering. + *

+ * Note: Needs to be called from the OpenGL rendering thread. + */ + private void initRendering() throws OpenGLException { + initializationRequired = false; + + // Create shaders. + panoSphereShader = new TransparencyShader(); + + // Create the filled panorama Sphere + float kRadius = 4.9f; + image.getTileProvider().setMaximumTextureSize(getMaxTextureSize()); + panoramaSphere = new PartialSphere(kRadius, textureLoaderManager); + panoramaSphere.setImage(image, textureLoaderManager); + panoramaSphere.setShader(panoSphereShader); + + // Misc. setup. + GLES20.glClearColor(Constants.BACKGROUND_BLACK[0], + Constants.BACKGROUND_BLACK[1], Constants.BACKGROUND_BLACK[2], + Constants.BACKGROUND_BLACK[3]); + + objectsInitialized = true; + + if (onInitializedCallback != null) { + onInitializedCallback.onCallback(null); + } + } + + /** + * Get the maximum supported texture size. + * + * @return the maximum supported texture size in pixels. + */ + private int getMaxTextureSize() { + int[] result = new int[1]; + GLES20.glGetIntegerv(GLES20.GL_MAX_TEXTURE_SIZE, result, 0); + return result[0]; + } +} diff --git a/paperscope/photosphere/lightcycle/viewer/Tile.java b/paperscope/photosphere/lightcycle/viewer/Tile.java new file mode 100644 index 00000000..87aca260 --- /dev/null +++ b/paperscope/photosphere/lightcycle/viewer/Tile.java @@ -0,0 +1,40 @@ +// Copyright 2012 Google Inc. All Rights Reserved. + +package com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.viewer; + +import android.graphics.Bitmap; + +/** + * A tile of a panorama image. + * + * @author haeberling@google.com (Sascha Haeberling) + */ +public class Tile { + /** The bitmap of the tile. */ + public final Bitmap bitmap; + /** The x-coordinate of the tile. */ + public final int x; + /** The y-coordinate of the tile. */ + public final int y; + /** The width of the tile in pixels. */ + public final int width; + /** The height of the tile in pixels. */ + public final int height; + + /** + * Create a new tile instance. + * + * @param bitmap the bitmap of the tile + * @param x the x-coordinate of the tile + * @param y the y-coordinate of the tile + * @param width the width of the tile in pixels + * @param height the height of the tile in pixels + */ + public Tile(Bitmap bitmap, int x, int y, int width, int height) { + this.bitmap = bitmap; + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } +} diff --git a/paperscope/photosphere/lightcycle/viewer/TileProvider.java b/paperscope/photosphere/lightcycle/viewer/TileProvider.java new file mode 100644 index 00000000..21e78a03 --- /dev/null +++ b/paperscope/photosphere/lightcycle/viewer/TileProvider.java @@ -0,0 +1,58 @@ +// Copyright 2012 Google Inc. All Rights Reserved. + +package com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.viewer; + +/** + * Provides tiles of an image. + * + * @author haeberling@google.com (Sascha Haeberling) + */ +public interface TileProvider { + + /** + * Returns the tile at the given coordinate. + * + * @param x the x-coordinate of the tile. + * @param y the y-coordinate of the tile. + * @return The tile at the given position or null. + */ + public Tile getTile(int x, int y); + + /** + * @return The size of the quadratic tiles (in pixels). + */ + public int getTileSize(); + + /** + * @return How many tiles there are horizontally. + */ + public int getTileCountX(); + + /** + * @return How many tiles there are vertically. + */ + public int getTileCountY(); + + /** + * @return The width of the rightmost column in pixels. + */ + public int getLastColumnWidth(); + + /** + * @return The height of the last row in pixels. + */ + public int getLastRowHeight(); + + /** + * Set the maximum texture size supported by the hardware. + * @param dimension the maximum texture size in pixels. + */ + public void setMaximumTextureSize(int dimension); + + /** + * Get the scale factor applied to all tile images. + * @return the scale factor. + */ + public float getScale(); + +} diff --git a/paperscope/photosphere/lightcycle/viewer/TileProviderImpl.java b/paperscope/photosphere/lightcycle/viewer/TileProviderImpl.java new file mode 100644 index 00000000..33dd2ad1 --- /dev/null +++ b/paperscope/photosphere/lightcycle/viewer/TileProviderImpl.java @@ -0,0 +1,176 @@ +// Copyright 2012 Google Inc. All Rights Reserved. + +package com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.viewer; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.BitmapFactory; +import android.graphics.BitmapRegionDecoder; +import android.graphics.Rect; +import android.util.Log; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import org.jspecify.annotations.Nullable; + +/** + * The default implementation of the {@link TileProvider} interface that loads + * tiles from an image input stream. + * + * @author haeberling@google.com (Sascha Haeberling) + */ +@TargetApi(10) +public class TileProviderImpl implements TileProvider { + @SuppressWarnings("unused") + private static final String TAG = TileProviderImpl.class.getSimpleName(); + /** + * The size of the tiles in pixels. 1024 seems to be an efficient value for the + * BitmapRegionDecoder. + */ + private static final int TILE_SIZE = 1024; + + // The maximum image size after downsampling. + private static final int MAX_DOWNSAMPLED_IMAGE_SIZE = 5000; + private final int tileCountX; + private final int tileCountY; + private final int lastColumnWidth; + private final int lastRowHeight; + + // How much we downsample the original image. + // If the input image is too big, we downsample it to make it more manageable. + private int downSamplingFactor = 1; + + private final BitmapRegionDecoder decoder; + + /** + * Creates a {@link TileProvider} from a file. + * + * @param imageFile the image file to use for tiling + * @return A {@link TileProvider} or null, if the file could not be read. + */ + public static @Nullable TileProvider fromFile(File imageFile) { + try { + return new TileProviderImpl(new FileInputStream(imageFile)); + } catch (FileNotFoundException ex) { + Log.e(TAG, "File not found", ex); + } + return null; + } + + /** + * Instantiates and initializes a new tile provider. + * + * @param image stream to use for tiling. + */ + @SuppressWarnings("deprecation") + public TileProviderImpl(InputStream image) { + try { + this.decoder = BitmapRegionDecoder.newInstance(image, false); + } catch (IOException e) { + throw new RuntimeException("Could not create decoder", e); + } + + // Compute the downsampling factor. The bigger the picture, the more we downsample it so that + // we're always below MAX_DOWNSAMPLED_IMAGE_SIZE. + this.downSamplingFactor = 1 + this.decoder.getWidth() / MAX_DOWNSAMPLED_IMAGE_SIZE; + + // Compute the size of the image after downsampling. + int downSampledWidth = this.decoder.getWidth() / downSamplingFactor; + int downSampledHeight = this.decoder.getHeight() / downSamplingFactor; + + double horizontalTileCount = downSampledWidth / (double) TILE_SIZE; + this.tileCountX = (int) Math.ceil(horizontalTileCount); + + int lastColumnWidthPixels = downSampledWidth % TILE_SIZE; + + this.lastColumnWidth = + lastColumnWidthPixels > 0 ? lastColumnWidthPixels : TILE_SIZE; + + double verticalTileCount = (downSampledHeight) / (double) TILE_SIZE; + this.tileCountY = (int) Math.ceil(verticalTileCount); + int lastRowHeightPixels = downSampledHeight % TILE_SIZE; + this.lastRowHeight = + lastRowHeightPixels > 0 ? lastRowHeightPixels : TILE_SIZE; + } + + @SuppressWarnings("deprecation") // This is ignored because our app minSdkVersion is 16. + @SuppressLint("NewApi") + @Override + public synchronized @Nullable Tile getTile(int x, int y) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Config.ARGB_8888; + options.inPreferQualityOverSpeed = true; + // Use our sampling factor here to sample the bitmap. + options.inSampleSize = downSamplingFactor; + + + // TODO(haeberling): Try to use level 11 API to recycle bitmaps. + Bitmap bitmap = this.decoder.decodeRegion( + getImageRegionForTileCoordinate(x, y), options); + if (bitmap == null) { + return null; + } + return new Tile(bitmap, x, y, TILE_SIZE, TILE_SIZE); + } + + @Override + public int getTileSize() { + return TILE_SIZE; + } + + @Override + public int getTileCountX() { + return tileCountX; + } + + @Override + public int getTileCountY() { + return tileCountY; + } + + @Override + public int getLastColumnWidth() { + return lastColumnWidth; + } + + @Override + public int getLastRowHeight() { + return lastRowHeight; + } + + /** + * Returns the region for the tile at the given tile coordinate. + * The coordinates are expressed in the original image coordinates before downsampling. + */ + private Rect getImageRegionForTileCoordinate(int tileIndexX, int tileIndexY) { + int originalTileSize = TILE_SIZE * downSamplingFactor; + int left = tileIndexX * originalTileSize; + int top = tileIndexY * originalTileSize; + int right = left + originalTileSize; + int bottom = top + originalTileSize; + + // For the last column and row we need to substract the padding, so the + // tiles will be smaller than the default tile size. + if (tileIndexX == this.tileCountX - 1) { + right -= (originalTileSize - lastColumnWidth * downSamplingFactor); + } + if (tileIndexY == this.tileCountY - 1) { + bottom -= (originalTileSize - lastRowHeight * downSamplingFactor); + } + return new Rect(left, top, right, bottom); + } + + @Override + public void setMaximumTextureSize(int dimension) { + // Nothing to do here. + } + + @Override + public float getScale() { + return 1.0f / downSamplingFactor; + } +} diff --git a/paperscope/photosphere/lightcycle/xmp/PanoConstants.java b/paperscope/photosphere/lightcycle/xmp/PanoConstants.java new file mode 100644 index 00000000..5dbd4e61 --- /dev/null +++ b/paperscope/photosphere/lightcycle/xmp/PanoConstants.java @@ -0,0 +1,61 @@ +// Copyright 2012 Google Inc. All Rights Reserved. + +package com.google.vr.cardboard.paperscope.demo.photosphere.lightcycle.xmp; + +import com.adobe.xmp.XMPException; +import com.adobe.xmp.XMPMetaFactory; + +/** + * Namespace and fields for panorama in XMP. + * TODO(bwuest): This should not be forked from lightcycle, but rather shared as a library, can + * be included through LightCycleCoreLib. + */ +public class PanoConstants { + public static final String GOOGLE_PANO_NAMESPACE = "http://ns.google.com/photos/1.0/panorama/"; + public static final String PANO_PREFIX = "GPano"; + + // Properties for Google panorama. + // Whether to show the image in a panorama viewer, the property type is boolean. + public static final String PROPERTY_USE_PANORAMA_VIEWER = "UsePanoramaViewer"; + + // Projection type used in the image file, the property type is open choice. + public static final String PROPERTY_PROJECTION_TYPE = "ProjectionType"; + public static final String PROPERTY_VALUE_EQUIRECTANGULAR = "equirectangular"; + public static final String PROPERTY_VALUE_CUBIC = "cubic"; + + // Date and time for the first and last image created in the panorama. + public static final String FIRST_PHOTO_DATE = "FirstPhotoDate"; + public static final String LAST_PHOTO_DATE = "LastPhotoDate"; + + // The count of source photos to create panorama. + public static final String SOURCE_PHOTOS_COUNT = "SourcePhotosCount"; + + /** Pose heading in degrees. */ + public static final String POSE_HEADING_DEGREES = "PoseHeadingDegrees"; + /** Pose pitch in degrees. */ + public static final String POSE_PITCH_DEGREES = "PosePitchDegrees"; + /** Pose roll in degrees. */ + public static final String POSE_ROLL_DEGREES = "PoseRollDegrees"; + + // Cropped area metadata. + public static final String CROPPED_AREA_IMAGE_WIDTH_PIXELS = + "CroppedAreaImageWidthPixels"; + public static final String CROPPED_AREA_IMAGE_HEIGHT_PIXELS = + "CroppedAreaImageHeightPixels"; + public static final String CROPPED_AREA_FULL_PANO_WIDTH_PIXELS = + "FullPanoWidthPixels"; + public static final String CROPPED_AREA_FULL_PANO_HEIGHT_PIXELS = + "FullPanoHeightPixels"; + public static final String CROPPED_AREA_LEFT = + "CroppedAreaLeftPixels"; + public static final String CROPPED_AREA_TOP = + "CroppedAreaTopPixels"; + + static { + try { + XMPMetaFactory.getSchemaRegistry().registerNamespace(GOOGLE_PANO_NAMESPACE, PANO_PREFIX); + } catch (XMPException e) { + e.printStackTrace(); + } + } +} diff --git a/paperscope/photosphere/lightcycle/xmp/XmpUtil.java b/paperscope/photosphere/lightcycle/xmp/XmpUtil.java new file mode 100644 index 00000000..4438599d --- /dev/null +++ b/paperscope/photosphere/lightcycle/xmp/XmpUtil.java @@ -0,0 +1,683 @@ +/* + * Copyright (C) 2013 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.vr.cardboard.paperscope.demo.photosphere.lightcycle.xmp; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import android.util.Log; +import com.adobe.xmp.XMPException; +import com.adobe.xmp.XMPMeta; +import com.adobe.xmp.XMPMetaFactory; +import com.adobe.xmp.options.SerializeOptions; +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Formatter; +import java.util.List; +import java.util.Locale; +import org.jspecify.annotations.Nullable; + +/** + * Util class to read/write xmp from a jpeg image file. It only supports jpeg image format, and + * doesn't support extended xmp now. To use it: XMPMeta xmpMeta = + * XmpUtil.extractOrCreateXMPMeta(filename); + * xmpMeta.setProperty(PanoConstants.GOOGLE_PANO_NAMESPACE, "property_name", "value"); + * XmpUtil.writeXMPMeta(filename, xmpMeta); + * + *

Or if you don't care the existing XMP meta data in image file: XMPMeta xmpMeta = + * XmpUtil.createXMPMeta(); xmpMeta.setPropertyBoolean(PanoConstants.GOOGLE_PANO_NAMESPACE, + * "bool_property_name", "true"); XmpUtil.writeXMPMeta(filename, xmpMeta); + */ +public class XmpUtil { + private static final String TAG = "XmpUtil"; + private static final int XMP_HEADER_SIZE = 29; + private static final String XMP_HEADER = "http://ns.adobe.com/xap/1.0/\0"; + private static final int MAX_XMP_BUFFER_SIZE = 65502; + + private static final String EXTENDED_XMP_HEADER_SIGNATURE = + "http://ns.adobe.com/xmp/extension/\0"; + private static final String XMP_NOTE_NAMESPACE = "http://ns.adobe.com/xmp/note/"; + private static final String NOTE_PREFIX = "xmpNote"; + + private static final int MAX_EXTENDED_XMP_BUFFER_SIZE = 65000; + private static final int EXTEND_XMP_HEADER_SIZE = 75; + + private static final String GOOGLE_PANO_NAMESPACE = "http://ns.google.com/photos/1.0/panorama/"; + private static final String PANO_PREFIX = "GPano"; + + private static final int M_SOI = 0xd8; // File start marker. + private static final int M_APP1 = 0xe1; // Marker for Exif or XMP. + private static final int M_SOS = 0xda; // Image data marker. + + // Jpeg file is composed of many sections and image data. This class is used + // to hold the section data from image file. + private static class Section { + int marker; + int length; + byte[] data; + } + + static { + try { + XMPMetaFactory.getSchemaRegistry().registerNamespace(GOOGLE_PANO_NAMESPACE, PANO_PREFIX); + + XMPMetaFactory.getSchemaRegistry().registerNamespace(XMP_NOTE_NAMESPACE, NOTE_PREFIX); + } catch (XMPException e) { + Log.e(TAG, "Error registering XMP namespace", e); + } + } + + /** + * Extracts XMPMeta from JPEG image file. + * + * @param filename JPEG image file name. + * @return Extracted XMPMeta or null. + */ + public static @Nullable XMPMeta extractXmpMeta(String filename) { + if (!isJpeg(filename)) { + Log.d(TAG, "XMP parse: only jpeg file is supported"); + return null; + } + + try { + return extractXmpMeta(new FileInputStream(filename)); + } catch (FileNotFoundException e) { + Log.e(TAG, "Could not read file: " + filename, e); + return null; + } + } + + /** + * Extracts XMPMeta from a JPEG image file stream. + * + * @param is the input stream containing the JPEG image file. + * @return Extracted XMPMeta or null. + */ + public static @Nullable XMPMeta extractXmpMeta(InputStream is) { + List

sections = parse(is, true); + if (sections == null) { + return null; + } + // Now we don't support extended xmp. + for (Section section : sections) { + if (hasStandardXmpHeader(section.data)) { + int end = getXmpContentEnd(section.data); + byte[] buffer = new byte[end - XMP_HEADER_SIZE]; + System.arraycopy(section.data, XMP_HEADER_SIZE, buffer, 0, buffer.length); + try { + return XMPMetaFactory.parseFromBuffer(buffer); + } catch (XMPException e) { + Log.d(TAG, "XMP parse error", e); + return null; + } + } + } + return null; + } + + /** Creates a new XMPMeta. */ + public static XMPMeta createXmpMeta() { + return XMPMetaFactory.create(); + } + + /** Tries to extract XMP meta from image file first, if failed, create one. */ + public static XMPMeta extractOrCreateXmpMeta(String filename) { + XMPMeta meta = extractXmpMeta(filename); + return meta == null ? createXmpMeta() : meta; + } + + /** Writes the XMPMeta to the jpeg image file. */ + public static boolean writeXmpMeta(String filename, XMPMeta meta) { + if (!isJpeg(filename)) { + Log.d(TAG, "XMP parse: only jpeg file is supported"); + return false; + } + List
sections = null; + try { + sections = parse(new FileInputStream(filename), /* readMetaOnly= */ false); + sections = insertXmpSection(sections, meta); + if (sections == null) { + return false; + } + } catch (FileNotFoundException e) { + Log.e(TAG, "Could not read file: " + filename, e); + return false; + } + try (FileOutputStream outputStream = new FileOutputStream(filename)) { + // Overwrite the image file with the new meta data. + writeJpegFile(outputStream, sections); + } catch (IOException e) { + Log.d(TAG, "Write file failed:" + filename, e); + return false; + } + return true; + } + + /** Updates a jpeg file from inputStream with XMPMeta to outputStream. */ + public static boolean writeXmpMeta( + InputStream inputStream, OutputStream outputStream, XMPMeta meta) { + List
sections = parse(inputStream, false); + sections = insertXmpSection(sections, meta); + if (sections == null) { + return false; + } + try { + // Overwrite the image file with the new meta data. + writeJpegFile(outputStream, sections); + } catch (IOException e) { + Log.d(TAG, "Write to stream failed", e); + return false; + } finally { + if (outputStream != null) { + try { + outputStream.close(); + } catch (IOException e) { + // Ignore. + } + } + } + return true; + } + + /** + * Updates a jpeg file from inputStream with XMPMeta to outputStream. + * + * @param inputStream Input image data stream + * @param outputStream Output image data stream + * @param standardMeta The main portion of the metadata tree must be serialized and written as the + * standard XMP packet + * @param extendedMeta The extended portion must be serialized without a packet wrapper, and + * written as a series of APP1 marker segments + * @return True if the XMP meta data is written successfully, false otherwise. + */ + public static boolean writeXmpMeta( + InputStream inputStream, + OutputStream outputStream, + XMPMeta standardMeta, + XMPMeta extendedMeta) { + byte[] buffer; + try { + SerializeOptions options = new SerializeOptions(); + options.setUseCompactFormat(true); + // We have to omit packet wrapper here because + // javax.xml.parsers.DocumentBuilder + // fails to parse the packet end in android. + options.setOmitPacketWrapper(true); + buffer = XMPMetaFactory.serializeToBuffer(extendedMeta, options); + } catch (XMPException e) { + Log.d(TAG, "Serialize extended xmp failed", e); + return false; + } + + String guid = getGuid(buffer); + try { + standardMeta.setProperty(XMP_NOTE_NAMESPACE, "HasExtendedXMP", guid); + } catch (XMPException exception) { + Log.d(TAG, "set XMPMeta Property", exception); + return false; + } + List
sections = parse(inputStream, false); + List
xmpSections = new ArrayList<>(); + Section standardXmpSection = createStandardXmpSection(standardMeta); + if (standardXmpSection == null) { + Log.e(TAG, "create standard meta section error"); + return false; + } + xmpSections.add(standardXmpSection); + + List
extendedSections = splitExtendXmpMeta(buffer, guid); + xmpSections.addAll(extendedSections); + sections = insertXmpSection(sections, xmpSections); + if (sections == null) { + Log.d(TAG, "Insert XMP fialed"); + return false; + } + try { + // Overwrite the image file with the new meta data. + writeJpegFile(outputStream, sections); + } catch (IOException e) { + Log.d(TAG, "Write to stream failed", e); + return false; + } finally { + if (outputStream != null) { + try { + outputStream.close(); + } catch (IOException e) { + // Ignore. + } + } + } + return true; + } + + /** Write a list of sections to a Jpeg file. */ + private static void writeJpegFile(OutputStream os, List
sections) throws IOException { + // Writes the jpeg file header. + os.write(0xff); + os.write(M_SOI); + for (Section section : sections) { + os.write(0xff); + os.write(section.marker); + if (section.length > 0) { + // It's not the image data. + int lh = section.length >> 8; + int ll = section.length & 0xff; + os.write(lh); + os.write(ll); + } + os.write(section.data); + } + } + + /** + * Checks whether the byte array has the standard XMP header. The standard XMP section contains a + * fixed length header XMP_HEADER. + * + * @param data Xmp metadata. + * @return True if the byte array has the standard XMP header, false otherwise. + */ + private static boolean hasStandardXmpHeader(byte[] data) { + if (data.length < XMP_HEADER_SIZE) { + return false; + } + byte[] header = Arrays.copyOf(data, XMP_HEADER_SIZE); + if (new String(header, UTF_8).equals(XMP_HEADER)) { + return true; + } + return false; + } + + /** + * Checks whether the byte array has the extended XMP header. Extended XMP sections start with + * EXTENDED_XMP_HEADER_SIGNATURE. + * + * @param data Xmp metadata. + * @return True if the byte array has the extended XMP header, false otherwise. + */ + private static boolean hasExtendedXmpHeader(byte[] data) { + if (data.length < EXTENDED_XMP_HEADER_SIGNATURE.length()) { + return false; + } + byte[] header = Arrays.copyOf(data, EXTENDED_XMP_HEADER_SIGNATURE.length()); + if (new String(header, UTF_8).equals(EXTENDED_XMP_HEADER_SIGNATURE)) { + return true; + } + return false; + } + + /** + * Checks if a section is an XMP section, either standard or extended. + * + * @param section The JPEG section to check. + * @return True if the section contains XMP data, false otherwise. + */ + private static boolean isXmpSection(Section section) { + return section.marker == M_APP1 + && (hasStandardXmpHeader(section.data) || hasExtendedXmpHeader(section.data)); + } + + /** + * Gets the end of the xmp meta content. If there is no packet wrapper, return data.length, + * otherwise return 1 + the position of last '>' without '?' before it. Usually the packet wrapper + * end is " but javax.xml.parsers.DocumentBuilder fails to parse it in android. + * + * @param data xmp metadata bytes. + * @return The end of the xmp metadata content. + */ + private static int getXmpContentEnd(byte[] data) { + for (int i = data.length - 1; i >= 1; --i) { + if (data[i] == '>') { + if (data[i - 1] != '?') { + return i + 1; + } + } + } + // It should not reach here for a valid xmp meta. + return data.length; + } + + /** + * Parses the jpeg image file. If readMetaOnly is true, only keeps the Exif and XMP sections (with + * marker M_APP1) and ignore others; otherwise, keep all sections. The last section with image + * data will have -1 length. + * + * @param is Input image data stream. + * @param readMetaOnly Whether only reads the metadata in jpg. + * @return The parse result. + */ + private static @Nullable List
parse(InputStream is, boolean readMetaOnly) { + try { + if (is.read() != 0xff || is.read() != M_SOI) { + return null; + } + List
sections = new ArrayList<>(); + int c; + while ((c = is.read()) != -1) { + if (c != 0xff) { + return null; + } + // Skip padding bytes. + while ((c = is.read()) == 0xff) {} + if (c == -1) { + return null; + } + int marker = c; + if (marker == M_SOS) { + // M_SOS indicates the image data will follow and no metadata after + // that, so read all data at one time. + if (!readMetaOnly) { + Section section = new Section(); + section.marker = marker; + section.length = -1; + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + int nRead; + byte[] data = new byte[16384]; + while ((nRead = is.read(data, 0, data.length)) != -1) { + buffer.write(data, 0, nRead); + } + buffer.flush(); + // For large panorama files, reading the entire entropy-coded data (ECD) into a + // single byte array can lead to OutOfMemoryError. A more memory-efficient approach + // would involve handling this section in a stream-based manner when writing, + // avoiding the need to load the entire content into memory at once. + section.data = buffer.toByteArray(); + sections.add(section); + } + return sections; + } + int lh = is.read(); + int ll = is.read(); + if (lh == -1 || ll == -1) { + return null; + } + int length = lh << 8 | ll; + if (!readMetaOnly || c == M_APP1) { + Section section = new Section(); + section.marker = marker; + section.length = length; + section.data = new byte[length - 2]; + int numBytesRead = 0; + int bytesToRead = length - 2; + while (numBytesRead < bytesToRead) { + int readResult = is.read(section.data, numBytesRead, bytesToRead - numBytesRead); + if (readResult == -1) { + break; // End of stream + } + numBytesRead += readResult; + } + if (numBytesRead != bytesToRead) { + Log.d(TAG, "Could not read all bytes for section."); + return null; + } + sections.add(section); + } else { + // Skip this section since all exif/xmp meta will be in M_APP1 + // section. + long bytesToSkip = length - 2L; + while (bytesToSkip > 0) { + long skipped = is.skip(bytesToSkip); + if (skipped <= 0) { + // Should not happen if length is positive, but handle for safety. + Log.d(TAG, "Could not skip all bytes for section."); + return null; + } + bytesToSkip -= skipped; + } + } + } + return sections; + } catch (IOException e) { + Log.d(TAG, "Could not parse file.", e); + return null; + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + // Ignore. + } + } + } + } + + private static @Nullable Section createStandardXmpSection(XMPMeta meta) { + byte[] buffer; + try { + SerializeOptions options = new SerializeOptions(); + options.setUseCompactFormat(true); + // We have to omit packet wrapper here because + // javax.xml.parsers.DocumentBuilder + // fails to parse the packet end in android. + options.setOmitPacketWrapper(true); + buffer = XMPMetaFactory.serializeToBuffer(meta, options); + } catch (XMPException e) { + Log.d(TAG, "Serialize xmp failed", e); + return null; + } + if (buffer.length > MAX_XMP_BUFFER_SIZE) { + Log.e(TAG, "exceed max size"); + return null; + } + // The XMP section starts with XMP_HEADER and then the real xmp data. + byte[] xmpdata = new byte[buffer.length + XMP_HEADER_SIZE]; + System.arraycopy(XMP_HEADER.getBytes(), 0, xmpdata, 0, XMP_HEADER_SIZE); + System.arraycopy(buffer, 0, xmpdata, XMP_HEADER_SIZE, buffer.length); + Section xmpSection = new Section(); + xmpSection.marker = M_APP1; + // Adds the length place (2 bytes) to the section length. + xmpSection.length = xmpdata.length + 2; + xmpSection.data = xmpdata; + + return xmpSection; + } + + private static @Nullable Section createSection(byte[] portionOfExtendedMeta, byte[] headerBytes) { + + if (portionOfExtendedMeta.length > MAX_EXTENDED_XMP_BUFFER_SIZE) { + // Do not support extended xmp now. + Log.e(TAG, "createSection fail exceed max size"); + return null; + } + + byte[] xmpdata = Arrays.copyOf(headerBytes, portionOfExtendedMeta.length + 75); + System.arraycopy(headerBytes, 0, xmpdata, 0, headerBytes.length); + + System.arraycopy( + portionOfExtendedMeta, 0, xmpdata, headerBytes.length, portionOfExtendedMeta.length); + Section xmpSection = new Section(); + xmpSection.marker = M_APP1; + // Adds the length place (2 bytes) to the section length. + xmpSection.length = xmpdata.length + 2; + xmpSection.data = xmpdata; + ByteBuffer byteBuffer2 = ByteBuffer.wrap(xmpdata); + Log.d(TAG, "fullLength=" + byteBuffer2.getInt(67) + " offset=" + byteBuffer2.getInt(71)); + return xmpSection; + } + + /** + * Split extendXMPMeta to multiple marker segments + * + * @param extendedXmpMetaBytes serialized extended XMP + * @param guid Is a 128-bit MD5 digest of the full ExtendedXMP serialization, stored as a 32-byte + * ASCII hex string + * @return split result + */ + private static @Nullable List
splitExtendXmpMeta( + byte[] extendedXmpMetaBytes, String guid) { + List
sections = new ArrayList<>(); + /* + The extended XMP JPEG marker segment content holds: + - a signature string, "http://ns.adobe.com/xmp/extension/\0" + - a 128 bit GUID stored as a 32 byte ASCII hex string + - a UInt32 full length of the entire extended XMP + - a UInt32 offset for this portion of the extended XMP + - the UTF-8 text for this portion of the extended XMP + */ + int splitNum = extendedXmpMetaBytes.length / MAX_EXTENDED_XMP_BUFFER_SIZE; + byte[] portion = new byte[MAX_EXTENDED_XMP_BUFFER_SIZE]; + ByteBuffer byteBuffer = ByteBuffer.wrap(extendedXmpMetaBytes); + Section extendedXmpSection = null; + + byte[] headerBytes = new byte[EXTEND_XMP_HEADER_SIZE]; + int index = 0; + System.arraycopy( + EXTENDED_XMP_HEADER_SIGNATURE.getBytes(), + 0, + headerBytes, + 0, + EXTENDED_XMP_HEADER_SIGNATURE.length()); + index += EXTENDED_XMP_HEADER_SIGNATURE.length(); + + System.arraycopy(guid.getBytes(), 0, headerBytes, index, guid.length()); + index += guid.length(); + + Log.d(TAG, "buffer.length=" + extendedXmpMetaBytes.length); + byte[] fullLengthBytes = new byte[4]; + ByteBuffer intBuffer = ByteBuffer.wrap(fullLengthBytes); + intBuffer.putInt(0, extendedXmpMetaBytes.length); + System.arraycopy(fullLengthBytes, 0, headerBytes, index, 4); + index += 4; + + byte[] offsetBytes = new byte[4]; + ByteBuffer offsetBuffer = ByteBuffer.wrap(offsetBytes); + for (int i = 0; i < splitNum; ++i) { + offsetBuffer.putInt(0, i * MAX_EXTENDED_XMP_BUFFER_SIZE); + System.arraycopy(offsetBytes, 0, headerBytes, index, 4); + + byteBuffer.get(portion); + extendedXmpSection = createSection(portion, headerBytes); + sections.add(extendedXmpSection); + } + + int remainSize = extendedXmpMetaBytes.length - splitNum * MAX_EXTENDED_XMP_BUFFER_SIZE; + if (remainSize > 0) { + offsetBuffer.putInt(0, splitNum * MAX_EXTENDED_XMP_BUFFER_SIZE); + System.arraycopy(offsetBytes, 0, headerBytes, index, 4); + + byte[] remain = new byte[remainSize]; + byteBuffer.get(remain); + extendedXmpSection = createSection(remain, headerBytes); + sections.add(extendedXmpSection); + } + + return sections; + } + + /** + * Inserts a list of XMP Sections into the overall list of JPEG sections. This method will replace + * any existing XMP sections found in the input {@code sections}. + * + * @param sections The list of JPEG sections parsed from the file. + * @param xmpSections The list of new XMP sections to insert. + * @return A new list of sections with the old XMP sections replaced by {@code xmpSections}, or + * null if the input is invalid. + */ + private static @Nullable List
insertXmpSection( + List
sections, List
xmpSections) { + if (sections == null || sections.isEmpty()) { + return null; + } + + List
resultSections = new ArrayList<>(); + int firstXmpIndex = -1; + int lastXmpIndex = -1; + + // Find the range of existing XMP sections (consecutive M_APP1 with standard or extended XMP + // headers). + for (int i = 0; i < sections.size(); i++) { + Section section = sections.get(i); + if (isXmpSection(section)) { + if (firstXmpIndex == -1) { + firstXmpIndex = i; + } + lastXmpIndex = i; + } else if (firstXmpIndex != -1) { + // Stop if a non-XMP section is encountered after finding XMP. + break; + } + } + + if (firstXmpIndex != -1) { + // Existing XMP sections found. Replace them with the new xmpSections. + resultSections.addAll(sections.subList(0, firstXmpIndex)); + resultSections.addAll(xmpSections); + if (lastXmpIndex + 1 < sections.size()) { + resultSections.addAll(sections.subList(lastXmpIndex + 1, sections.size())); + } + } else { + // No existing XMP sections. Determine insertion point. + // If the first section is M_APP1 (e.g., Exif), insert after it. + // Otherwise, make xmp data the first section. + int insertPosition = 0; + if (!sections.isEmpty() && sections.get(0).marker == M_APP1) { + insertPosition = 1; + } + resultSections.addAll(sections.subList(0, insertPosition)); + resultSections.addAll(xmpSections); + resultSections.addAll(sections.subList(insertPosition, sections.size())); + } + return resultSections; + } + + private static @Nullable List
insertXmpSection(List
sections, XMPMeta meta) { + if (sections == null || sections.size() <= 1) { + return null; + } + Section standardXmpSection = createStandardXmpSection(meta); + if (standardXmpSection == null) { + return null; + } + List
xmpSections = new ArrayList<>(); + xmpSections.add(standardXmpSection); + return insertXmpSection(sections, xmpSections); + } + + private static boolean isJpeg(String filename) { + return filename.toLowerCase(Locale.ROOT).endsWith(".jpg") + || filename.toLowerCase(Locale.ROOT).endsWith(".jpeg"); + } + + private static @Nullable String getGuid(byte[] src) { + StringBuilder builder = new StringBuilder(); + try { + MessageDigest digester = MessageDigest.getInstance("MD5"); + digester.update(src); + byte[] digest = digester.digest(); + + Formatter formatter = new Formatter(builder); + for (int i = 0; i < digest.length; ++i) { + formatter.format("%02x", ((256 + digest[i]) % 256)); + } + } catch (NoSuchAlgorithmException exception) { + Log.d(TAG, "get md5 instance failure" + exception); + return null; + } + + return builder.toString().toUpperCase(Locale.ROOT); + } + + private XmpUtil() {} +} diff --git a/paperscope/proguard.pgcfg b/paperscope/proguard.pgcfg new file mode 100644 index 00000000..9584a390 --- /dev/null +++ b/paperscope/proguard.pgcfg @@ -0,0 +1,96 @@ +# Keep methods in Activity that could be used in the XML attribute onClick +-keepclassmembers class * extends android.app.Activity { + public void *(android.view.View); +} + +# Keep setters in Views so that animations can still work. +-keepclassmembers public class * extends android.view.View { + public (android.content.Context); + public (android.content.Context, android.util.AttributeSet); +} + +# Keep classes and class members annotated with UsedByReflection. +-keep @com.google.android.apps.common.proguard.UsedByReflection class * +-keepclassmembers class * { + @com.google.android.apps.common.proguard.UsedByReflection *; +} + +# Keep CardboardView from the toolkit. Used in some demo layouts. +-keepnames public class com.google.vrtoolkit.cardboard.CardboardView + +# Keep carton and demo activities, including native VrApps. +-keepnames public class com.google.vr.cardboard.paperscope.carton.* extends android.app.Activity +-keepnames public class com.google.vr.cardboard.paperscope.*.*Demo extends android.app.Activity +-keepnames public class com.google.vr.app.*.* extends android.app.Activity + +# Keep the Android support library. +# Removed to fix b/79094005 +# -keep class android.support.** { *; } + +# Keep HudController and its inner classes and references in the maps demo. +-keep class com.google.vr.cardboard.paperscope.maps.HudController* { *; } +-keep class com.google.vr.cardboard.paperscope.maps.CameraManager { *; } + +# Whitelist classes that call Java from native code in Mirth. +-keep public class com.google.geo.render.mirth.MirthDisplay { *; } + +# Whitelist for the Windy Day demo. +-keep class com.motorola.moxie.common.* { *; } +-keep class com.motorola.windyday.JNI* { *; } + +# Whitelist for the Barhopper api. +-keep class com.google.android.libraries.barhopper.* { *; } + +# Whitelist for Picasso. +-dontwarn com.squareup.okhttp.** +-keep class com.squareup.okhttp.** { *; } +-keep interface com.squareup.okhttp.** { *; } + +# Whitelist for Volley +-dontwarn com.google.android.volley.AndroidHttpClient + +# Silence a myriad of warnings and notes. +-dontwarn android.annotation.** +-dontwarn android.support.** +-dontwarn com.google.common.** +-dontwarn com.google.android.apps.lightcycle.** +-dontwarn com.google.geo.render.mirth.** +-dontwarn com.motorola.avatar.** +-dontwarn org.ejml.ops.** + +-dontnote android.annotation.** +-dontnote android.support.** +-dontnote com.google.android.apps.analytics.** +-dontnote com.google.android.apps.lightcycle.** +-dontnote com.google.common.** +-dontnote com.google.geo.render.mirth.** +-dontnote com.motorola.avatar.** +-dontnote com.motorola.moxie.** +-dontnote org.ejml.ops.** + +# android.app.Notification.setLatestEventInfo() was removed in MNC, but is +# still referenced (safely) by the following code - make sure it doesn't break +# the build. +-dontwarn com.google.android.gms.common.GooglePlayServicesUtil +-dontwarn com.google.android.gms.gcm.** + +# Unity +-dontwarn com.unity3d.** +-dontwarn org.fmod.** +-keep class com.unity3d.** { *; } +-keep class org.fmod.** { *; } +-keep class com.EasyMovieTexture.** { *; } +-keep class com.google.vr.platform.unity.** { *; } + +# These should be in a Cardboard SDK proguard.cfg file: b/28384324 +-keep public class com.google.vr.sdk.widgets.pano.VrPanoramaEventListener { *; } +-keep public class com.google.vr.sdk.widgets.pano.VrPanoramaView$Options { *; } + +# Needed to work around a bug causing a crash in ReleaseStartupUiTest when +# calling any public method from ScanlineRacingRenderer.onDrawFrame +-keep class com.google.vr.cardboard.ScanlineRacingRenderer { *; } + + +# Needed with GmsCore v16. +-keep class com.google.android.gms.measurement.AppMeasurement { *; } +-dontwarn com.google.android.gms.measurement.AppMeasurement diff --git a/paperscope/res/drawable-hdpi/logo.png b/paperscope/res/drawable-hdpi/logo.png new file mode 100644 index 00000000..49460157 Binary files /dev/null and b/paperscope/res/drawable-hdpi/logo.png differ diff --git a/paperscope/res/drawable-hdpi/transition.png b/paperscope/res/drawable-hdpi/transition.png new file mode 100644 index 00000000..336bbdbb Binary files /dev/null and b/paperscope/res/drawable-hdpi/transition.png differ diff --git a/paperscope/res/drawable-mdpi/cardboard_demo.png b/paperscope/res/drawable-mdpi/cardboard_demo.png new file mode 100644 index 00000000..85b87a98 Binary files /dev/null and b/paperscope/res/drawable-mdpi/cardboard_demo.png differ diff --git a/paperscope/res/drawable-mdpi/logo.png b/paperscope/res/drawable-mdpi/logo.png new file mode 100644 index 00000000..0fa96a25 Binary files /dev/null and b/paperscope/res/drawable-mdpi/logo.png differ diff --git a/paperscope/res/drawable-mdpi/settings_banner.jpg b/paperscope/res/drawable-mdpi/settings_banner.jpg new file mode 100644 index 00000000..5c474fff Binary files /dev/null and b/paperscope/res/drawable-mdpi/settings_banner.jpg differ diff --git a/paperscope/res/drawable-mdpi/transition.png b/paperscope/res/drawable-mdpi/transition.png new file mode 100644 index 00000000..5e7bd476 Binary files /dev/null and b/paperscope/res/drawable-mdpi/transition.png differ diff --git a/paperscope/res/drawable-xhdpi/cardboard_demo.png b/paperscope/res/drawable-xhdpi/cardboard_demo.png new file mode 100644 index 00000000..9d30aaff Binary files /dev/null and b/paperscope/res/drawable-xhdpi/cardboard_demo.png differ diff --git a/paperscope/res/drawable-xhdpi/logo.png b/paperscope/res/drawable-xhdpi/logo.png new file mode 100644 index 00000000..18227d58 Binary files /dev/null and b/paperscope/res/drawable-xhdpi/logo.png differ diff --git a/paperscope/res/drawable-xxhdpi/cardboard_demo.png b/paperscope/res/drawable-xxhdpi/cardboard_demo.png new file mode 100644 index 00000000..68a53c12 Binary files /dev/null and b/paperscope/res/drawable-xxhdpi/cardboard_demo.png differ diff --git a/paperscope/res/drawable-xxhdpi/cardboard_ic_launcher.png b/paperscope/res/drawable-xxhdpi/cardboard_ic_launcher.png new file mode 100644 index 00000000..71e9e256 Binary files /dev/null and b/paperscope/res/drawable-xxhdpi/cardboard_ic_launcher.png differ diff --git a/paperscope/res/drawable-xxhdpi/logo.png b/paperscope/res/drawable-xxhdpi/logo.png new file mode 100644 index 00000000..39ad8d92 Binary files /dev/null and b/paperscope/res/drawable-xxhdpi/logo.png differ diff --git a/paperscope/res/drawable-xxhdpi/rippleable.xml b/paperscope/res/drawable-xxhdpi/rippleable.xml new file mode 100644 index 00000000..261cc2d9 --- /dev/null +++ b/paperscope/res/drawable-xxhdpi/rippleable.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/paperscope/res/drawable-xxxhdpi/cardboard_demo.png b/paperscope/res/drawable-xxxhdpi/cardboard_demo.png new file mode 100644 index 00000000..aaae3d91 Binary files /dev/null and b/paperscope/res/drawable-xxxhdpi/cardboard_demo.png differ diff --git a/paperscope/res/drawable-xxxhdpi/logo.png b/paperscope/res/drawable-xxxhdpi/logo.png new file mode 100644 index 00000000..ca7ed5e3 Binary files /dev/null and b/paperscope/res/drawable-xxxhdpi/logo.png differ diff --git a/paperscope/res/drawable/ic_arrow_forward.xml b/paperscope/res/drawable/ic_arrow_forward.xml new file mode 100644 index 00000000..8759574c --- /dev/null +++ b/paperscope/res/drawable/ic_arrow_forward.xml @@ -0,0 +1,9 @@ + + + diff --git a/paperscope/res/drawable/ic_close.xml b/paperscope/res/drawable/ic_close.xml new file mode 100644 index 00000000..578f7436 --- /dev/null +++ b/paperscope/res/drawable/ic_close.xml @@ -0,0 +1,9 @@ + + + diff --git a/paperscope/res/drawable/ic_settings.xml b/paperscope/res/drawable/ic_settings.xml new file mode 100644 index 00000000..9af0972a --- /dev/null +++ b/paperscope/res/drawable/ic_settings.xml @@ -0,0 +1,9 @@ + + + diff --git a/paperscope/res/drawable/roundcorner.xml b/paperscope/res/drawable/roundcorner.xml new file mode 100644 index 00000000..c6430b4f --- /dev/null +++ b/paperscope/res/drawable/roundcorner.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/paperscope/res/layout/activity_vr.xml b/paperscope/res/layout/activity_vr.xml new file mode 100644 index 00000000..e40790d1 --- /dev/null +++ b/paperscope/res/layout/activity_vr.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + diff --git a/paperscope/res/layout/app_grid_card.xml b/paperscope/res/layout/app_grid_card.xml new file mode 100644 index 00000000..d520687f --- /dev/null +++ b/paperscope/res/layout/app_grid_card.xml @@ -0,0 +1,37 @@ + + + + + + + + + diff --git a/paperscope/res/layout/magic_window.xml b/paperscope/res/layout/magic_window.xml new file mode 100644 index 00000000..8b92be00 --- /dev/null +++ b/paperscope/res/layout/magic_window.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/paperscope/res/layout/my_library.xml b/paperscope/res/layout/my_library.xml new file mode 100644 index 00000000..7de95015 --- /dev/null +++ b/paperscope/res/layout/my_library.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + diff --git a/paperscope/res/layout/my_library_fragment.xml b/paperscope/res/layout/my_library_fragment.xml new file mode 100644 index 00000000..7001c03e --- /dev/null +++ b/paperscope/res/layout/my_library_fragment.xml @@ -0,0 +1,19 @@ + + + + diff --git a/paperscope/res/layout/transition_view.xml b/paperscope/res/layout/transition_view.xml new file mode 100644 index 00000000..c60fecb0 --- /dev/null +++ b/paperscope/res/layout/transition_view.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/paperscope/res/menu/my_library_settings_menu.xml b/paperscope/res/menu/my_library_settings_menu.xml new file mode 100644 index 00000000..cdf9c195 --- /dev/null +++ b/paperscope/res/menu/my_library_settings_menu.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + diff --git a/paperscope/res/menu/settings_menu.xml b/paperscope/res/menu/settings_menu.xml new file mode 100644 index 00000000..c8d3463e --- /dev/null +++ b/paperscope/res/menu/settings_menu.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/paperscope/res/values/colors.xml b/paperscope/res/values/colors.xml new file mode 100644 index 00000000..8e5a48a2 --- /dev/null +++ b/paperscope/res/values/colors.xml @@ -0,0 +1,17 @@ + + + #2979ff + #2979ff + #2962ff + #555 + #FFEA00 + #b4b4b4 + @android:color/transparent + @android:color/white + #4cFFFFFF + #ff6e40 + #ff0000 + #d8000000 + #B2FFFFFF + #DD000000 + diff --git a/paperscope/res/values/dimens.xml b/paperscope/res/values/dimens.xml new file mode 100644 index 00000000..4323860c --- /dev/null +++ b/paperscope/res/values/dimens.xml @@ -0,0 +1,6 @@ + + + 24mm + 2dip + 57dip + \ No newline at end of file diff --git a/paperscope/res/values/strings.xml b/paperscope/res/values/strings.xml new file mode 100644 index 00000000..9a6d539b --- /dev/null +++ b/paperscope/res/values/strings.xml @@ -0,0 +1,192 @@ + + + + + Cardboard + + + + Cardboard + + Cardboard Dev + + + + Cardboard + + + + Cardboard + + + + Switch Cardboard viewer + + + Pair your phone with your viewer for the best experience. + + + Let\'s get you set up + + + YES! + + + GET ONE + + + No Cardboard viewer? + + + MY LIBRARY + + + Cardboard Demos + + + Unable to open app + + + Help and Feedback + + + About + + + Privacy Policy + + + Contact Us + + + Could not open the requested web page. + + + Switch + + Using a different viewer? + + Place your phone into your Cardboard viewer + + Switch viewer + + Your phone is configured for the following viewer: + + Default Viewer + Settings + + + + "from " + + Unknown + + + + Storage permission is required to load images and videos for the Photosphere and + MyVideos demo experiences, respectively. + + + + Take videos with your phone and watch them here + + Playing + + + + Rate the app + + + Terms of service + + + Product safety + + Licenses + + App version + + diff --git a/paperscope/res/values/styles.xml b/paperscope/res/values/styles.xml new file mode 100644 index 00000000..3e5d4bf7 --- /dev/null +++ b/paperscope/res/values/styles.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/paperscope/settings/AndroidManifest.xml b/paperscope/settings/AndroidManifest.xml new file mode 100644 index 00000000..3f030414 --- /dev/null +++ b/paperscope/settings/AndroidManifest.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/paperscope/settings/SettingsActivity.java b/paperscope/settings/SettingsActivity.java new file mode 100644 index 00000000..fcd996ad --- /dev/null +++ b/paperscope/settings/SettingsActivity.java @@ -0,0 +1,121 @@ +package com.google.vr.cardboard.paperscope.demo.settings; + +import com.google.vr.cardboard.paperscope.demo.R; + +import android.content.res.AssetManager; +import android.os.Bundle; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup.MarginLayoutParams; +import android.widget.Button; +import android.widget.TextView; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; +import com.google.cardboard.sdk.CardboardViewApi; +import com.google.common.base.Strings; + +/** Activity for settings. */ +public class SettingsActivity extends AppCompatActivity { + static { + System.loadLibrary("cardboard_jni"); + } + + private long nativeApp; + private CardboardViewApi cardboardViewApi; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + nativeApp = nativeOnCreate(getAssets()); + + setContentView(R.layout.settings); + + // From SDK 35+, Screen displays in Edge-to-edge mode, which means that the system bars are + // drawn over the content. To make sure that the content is not covered by the system bars, we + // need to add the system bars padding to the content. + View container = findViewById(R.id.container); + ViewCompat.setOnApplyWindowInsetsListener( + container, + (v, windowInsets) -> { + Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()); + + MarginLayoutParams mlp = (MarginLayoutParams) v.getLayoutParams(); + mlp.leftMargin = insets.left; + mlp.bottomMargin = insets.bottom; + mlp.topMargin = insets.top; + mlp.rightMargin = insets.right; + v.setLayoutParams(mlp); + + return WindowInsetsCompat.CONSUMED; + }); + + Button switchViewerBtn = findViewById(R.id.switch_headset); + switchViewerBtn.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + nativeSwitchViewer(nativeApp); + } + }); + + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(true); + } + + cardboardViewApi = new CardboardViewApi(this); + } + + @Override + protected void onResume() { + super.onResume(); + + if (cardboardViewApi.hasSavedDeviceParams()) { + updateViewerInfo(); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + // Home button clicked. + finish(); + return true; + } + return super.onOptionsItemSelected(item); + } + + /** Updates the viewer info in the UI. */ + private void updateViewerInfo() { + TextView viewerModel = findViewById(R.id.headset_model); + viewerModel.setText(getViewerModel()); + TextView viewerVendor = findViewById(R.id.headset_vendor); + viewerVendor.setText(getViewerVendor()); + } + + /** Returns the viewer model. */ + private String getViewerModel() { + String viewerModel = cardboardViewApi.getSavedDeviceParams().getModel(); + if (Strings.isNullOrEmpty(viewerModel)) { + return getString(R.string.unknown_headset_info); + } + return viewerModel; + } + + /** Returns the viewer vendor. */ + private String getViewerVendor() { + String viewerVendor = cardboardViewApi.getSavedDeviceParams().getVendor(); + if (Strings.isNullOrEmpty(viewerVendor)) { + return getString(R.string.headset_vendor_prefix) + getString(R.string.unknown_headset_info); + } + return getString(R.string.headset_vendor_prefix) + viewerVendor; + } + + private native long nativeOnCreate(AssetManager assetManager); + + private native void nativeSwitchViewer(long nativeApp); +} diff --git a/paperscope/settings/res/drawable/settings_banner.jpg b/paperscope/settings/res/drawable/settings_banner.jpg new file mode 100644 index 00000000..5c474fff Binary files /dev/null and b/paperscope/settings/res/drawable/settings_banner.jpg differ diff --git a/paperscope/settings/res/layout/settings.xml b/paperscope/settings/res/layout/settings.xml new file mode 100644 index 00000000..9bd15416 --- /dev/null +++ b/paperscope/settings/res/layout/settings.xml @@ -0,0 +1,23 @@ + + + + + + + + + + diff --git a/paperscope/settings/res/layout/settings_shared.xml b/paperscope/settings/res/layout/settings_shared.xml new file mode 100644 index 00000000..05dadbc0 --- /dev/null +++ b/paperscope/settings/res/layout/settings_shared.xml @@ -0,0 +1,35 @@ + + + + + + + + + +