Últimamente hemos estado enfrascados en procesamiento de imágenes, pero en este post, y para cambiar un poco de tema a modo de paréntesis, nos introduciremos en la realidad virtual. Gracias a las gafas asequibles como las CardBoard, las más potentes como las Oculus o HTC Vive o las intermedias pero entretenidas PlayStationVR, en el año 2016 la expansión de la Realidad Virtual fue enorme. Pero tranquilos, nosotros no nos quedaremos atrás gracias a Google VR.

Para crear experiencias de realidad en nuestras aplicaciones Google ha puesto a disposición de los desarrolladores la plataforma Google VR. Esta plataforma proporciona todo lo necesario para desarrollar aplicaciones incluyendo, librerías, ejemplos, directrices, etc. Todo ello disponible gracias a las siguientes APIs:

  • Unity: Api que permite añadir soporte de RV a una aplicación Unity3D o crear una desde inicio.
  • Android: Permite crear aplicaciones que visualicen fotos y vídeos en 3D, audio espacial, detectar movimientos de la cabeza,etc.
  • iOS: Permite crear experiencias de realidad virtual de forma nativa utilizando Objective-C.
  • Unreal Engine: Soporta nativamente Google VR para experiencias de realidad virtual en aplicaciones móviles.

Para cada API disponemos de documentación y ejemplos que podremos utilizar para familiarizarnos con esta tecnología. Nosotros, como en todos los posts anteriores, nos centraremos en versiones de Android.

En este caso, la API facilitará al desarrollador a la hora de:

  • Corregir la distorsión de las lentes.
  • Audio espacial.
  • Seguimiento de la cabeza.
  • Calibración 3D.
  • Visualización lado-a-lado.
  • Configuración de geometría estéreo.
  • Manejo de eventos de usuario.

En los ejemplos que se pueden consultar tenemos diferentes aplicaciones que nos pueden resultar muy útiles para añadir contenido de realidad virtual a nuestra aplicación. Incluyen las más utilizadas que son la visualización de fotos y videos de 360.

A continuación, crearemos una aplicación realmente simple en la que visualizaremos una foto. El usuario podrá interactuar moviendo el teléfono, arrastrando la imagen o utilizando las gafas de Realidad Virtual. ¡Empecemos!

Descargar contenido a mostrar

Lo primero que debemos hacer es conseguir contenido válido para la visualización. Para no utilizar el ejemplo que proporciona Google lo ideal es utilizar uno nuestro. La creación de fotos panorámicas o de 360 puede ser dificultosa (más que nada por obtener imágenes de calidad) por lo que aprovecharemos imágenes de Google Street View.

Gracias a streetviewdownload.eu podemos descargar cualquier imagen capturada por Google y utilizarla en nuestra aplicación de una manera muy sencilla. Los pasos a seguir los podéis consultar en este video.

En nuestro caso, hemos descargado una imagen cercana al campo base del Everest.

Jugando con el código
  1. Creamos una nueva aplicación con la plantilla “Empty Activity” y le fijamos el SDK mínimo en 25 (es obligatorio).
  2. Nos aseguramos que en el gradle del proyecto está el jcenter.
    allprojects {
        repositories {
            jcenter()
        }
    }
    
  3. En el gradle de la aplicación, en el apartado de las dependencias, añadimos el siguiente y sincronizamos:
    compile 'com.google.vr:sdk-panowidget:1.70.0'
  4. Toca añadir el jpg que queremos mostrar en el Proyecto. En el árbol de proyecto pulsamos con el botón derecho sobre app y seleccionamos New/Folder/Assets Folder y copiamos el fichero. En nuestro caso es everest.jpg.
  5. En el layout de la actividad vaciamos el layout principal y le añadimos la siguiente vista:
    <com.google.vr.sdk.widgets.pano.VrPanoramaView
        android:id="@+id/pano_view"
        android:layout_margin="5dip"
        android:layout_width="match_parent"
        android:scrollbars="@null"
        android:layout_height="250dip"/>
    

    Esta vista está en la librería añadida en el paso 3 y es el visor de imágenes de 360 grados.

  6. Abrimos el MainActivity y añadimos las variables que vamos a utilizar:
    private static final String TAG = MainActivity.class.getSimpleName();
    private VrPanoramaView panoWidgetView;
    public boolean loadImageSuccessful;
    private Uri fileUri;
    private Options panoOptions = new Options();
    private ImageLoaderTask backgroundImageLoaderTask;
    

    El tag lo utilizaremos en los logs.
    Tendremos la variable de la vista de panorama del layout, un boolean para saber si la imagen se ha cargado correctamente, un Uri para el fichero a cargar, un Options para fijar las características del panorama y un hilo para cargar la imagen en paralelo, evitando el bloqueo del hilo principal.

  7. En el método onCreate añadimos este código:
    panoWidgetView = (VrPanoramaView) findViewById(R.id.pano_view);
    panoWidgetView.setEventListener(new ActivityEventListener());
    
    handleIntent(getIntent());
    

    Inicializamos la variable del panorama y añadimos un método que se llamará en el inicio de la actividad y los cambios de rotación.

  8. Añadimos el método handleIntent
    private void handleIntent(Intent intent) {
        if (Intent.ACTION_VIEW.equals(intent.getAction())) {
            Log.i(TAG, "ACTION_VIEW Intent recieved");
    
            fileUri = intent.getData();
            if (fileUri == null) {
                Log.w(TAG, "No data uri specified. Use \"-d /path/filename\".");
            } else {
                Log.i(TAG, "Using file " + fileUri.toString());
            }
    
            panoOptions.inputType = intent.getIntExtra("inputType", Options.TYPE_MONO);
            Log.i(TAG, "Options.inputType = " + panoOptions.inputType);
        } else {
            Log.i(TAG, "Intent is not ACTION_VIEW. Using default pano image.");
            fileUri = null;
            panoOptions.inputType = Options.TYPE_MONO;
        }
    
        if (backgroundImageLoaderTask != null) {
            backgroundImageLoaderTask.cancel(true);
        }
        backgroundImageLoaderTask = new ImageLoaderTask();
        backgroundImageLoaderTask.execute(Pair.create(fileUri, panoOptions));
    }

    Si el intent tiene un fichero para cargar se prepara y se carga en un hilo aparte con unas opciones concretas.

  9. Añadimos los métodos que nos interesa sobrescribir.
    @Override
    protected void onNewIntent(Intent intent) {
        Log.i(TAG, this.hashCode() + ".onNewIntent()");
        setIntent(intent);
        handleIntent(intent);
    }
    @Override
    protected void onPause() {
        panoWidgetView.pauseRendering();
        super.onPause();
    }
    
    @Override
    protected void onResume() {
        super.onResume();
        panoWidgetView.resumeRendering();
    }
    
    @Override
    protected void onDestroy() {
        panoWidgetView.shutdown();
    
        if (backgroundImageLoaderTask != null) {
            backgroundImageLoaderTask.cancel(true);
        }
        super.onDestroy();
    }

    Controlaremos la visualización del panorama y si el funcionamiento del hilo encargado de cargar la imagen.

  10. Añadimos el hilo que se encargará de cargar la imagen:
    class ImageLoaderTask extends AsyncTask<Pair<Uri, Options>, Void, Boolean> {
    
        @Override
        protected Boolean doInBackground(Pair<Uri, Options>... fileInformation) {
            Options panoOptions = null;  // It's safe to use null VrPanoramaView.Options.
            InputStream istr = null;
            if (fileInformation == null || fileInformation.length < 1
                    || fileInformation[0] == null || fileInformation[0].first == null) {
                AssetManager assetManager = getAssets();
                try {
                    istr = assetManager.open("everest.jpg");
                    panoOptions = new Options();
                    panoOptions.inputType = Options.TYPE_MONO;
                } catch (IOException e) {
                    Log.e(TAG, "Could not decode default bitmap: " + e);
                    return false;
                }
            } else {
                try {
                    istr = new FileInputStream(new File(fileInformation[0].first.getPath()));
                    panoOptions = fileInformation[0].second;
                } catch (IOException e) {
                    Log.e(TAG, "Could not load file: " + e);
                    return false;
                }
            }
    
            panoWidgetView.loadImageFromBitmap(BitmapFactory.decodeStream(istr), panoOptions);
            try {
                istr.close();
            } catch (IOException e) {
                Log.e(TAG, "Could not close input stream: " + e);
            }
    
            return true;
        }
    }

    Carga la imagen desde el fichero y espera a que se cargue en el panorama.

  11. Para finalizar debemos añadir la clase escuchadora del panorama. Lo utilizaremos para saber si la imagen se ha cargado correctamente o no.
    private class ActivityEventListener extends VrPanoramaEventListener {
       @Override
        public void onLoadSuccess() {
            loadImageSuccessful = true;
        }
    
       @Override
        public void onLoadError(String errorMessage) {
            loadImageSuccessful = false;
            Toast.makeText(
                    MainActivity.this, "Error loading pano: " + errorMessage, Toast.LENGTH_LONG)
                    .show();
            Log.e(TAG, "Error loading pano: " + errorMessage);
        }
    }
  12. Ejecutamos y disfrutamos de la imagen en 3D.

Como podéis observar utilizar estos recursos nos facilitan la visualización en diferentes formas. Si ponemos la imagen a pantalla completa podemos cambiar la vista a modo Visor (Cardboard) sin que tengamos que preocuparnos de su programación.

De esta forma, y de manera muy sencilla, podemos añadir componentes de realidad virtual a nuestras aplicaciones nativas.

En posts venideros veremos más opciones que nos ofrece Google VR. Mientras tanto ¡seguid programando a tope!

El código de este ejemplo lo podéis descargar desde aquí.