Tech Note: Animated Loading Screen
Oculus Developer Blog
|
Posted by Tevor Dasch
|
October 25, 2018
|
Share

The long standing recommendation for loading screens has been to display a static texture in a TimeWarp Overlay, and then synchronously load any needed assets. This works, because TimeWarp will continue tracking the player's head while the main thread does all the loading work. However, static loading screens are boring. We have long supported displaying a spinner, but we haven't offered much in the way of customizing this.

Well now there is a way to animate your custom loading screen, and the way to do this is by taking advantage of Android's Surface. In Oculus Mobile SDK 1.15 and later it is possible to create a SwapChain that wraps an Android Surface. TimeWarp will then automatically update the swap chain texture when drawing to the screen. This lets you have a dynamic texture without calling SubmitLayers.

The basic way to do this is as follows: Create a swapchain using vrapi_CreateAndroidSurfaceSwapChain(..), attach this swap chain to a layer, submit the layer, then use vrapi_GetTextureSwapChainAndroidSurface(..) to retrieve the Surface object to pass to some java method.

For example, using code like this:

const int textureWidth = 512;
const int textureHeight = 512;
  	
ovrLayerProjection2 layer = vrapi_DefaultLayerProjection2();
    
const ovrMatrix4f projectionMatrix            = ovrMatrix4f_CreateProjectionFov( 30.0f, 30.0f, 0.0f, 0.0f, 0.1f, 0.0f );
const ovrMatrix4f texCoordsFromTanAngles    = ovrMatrix4f_TanAngleMatrixFromProjection( &projectionMatrix );
    
ovrTextureSwapChain * swapChain = vrapi_CreateAndroidSurfaceSwapChain(textureWidth, textureHeight);
    
layer.Textures[0].ColorSwapChain = layer.Textures[1].ColorSwapChain = swapChain;
layer.Textures[0].TexCoordsFromTanAngles = layer.Textures[1].TexCoordsFromTanAngles = texCoordsFromTanAngles;
    
JNIEnv * jni = app->GetJava()->Env;

const jclass loadingScreenClass = jni->FindClass("com/oculus/loadingscreen/LoadingScreen");
const jmethodID startUpdatingLoadingScreenMethodID = jni->GetStaticMethodID( mainActivityClass, "startUpdatingLoadingScreen", "(Landroid/view/Surface;II)V" );
const jmethodID stopUpdatingLoadingScreenMethodID = jni->GetStaticMethodID( mainActivityClass, "stopUpdatingLoadingScreen", "()V" );
 
// send surface texture to java for animation   
jni->CallStaticVoidMethod(loadingScreenClass, startUpdatingLoadingScreenMethodID, vrapi_GetTextureSwapChainAndroidSurface(swapChain), textureWidth, textureHeight);
    
const ovrLayerHeader2 * layers[] =
{
	&layer.Header
};
  	
ovrSubmitFrameDescription2 frameDesc = {};
frameDesc.Flags = VRAPI_FRAME_FLAG_FLUSH;
frameDesc.SwapInterval = 1;
frameDesc.FrameIndex = 0;
frameDesc.DisplayTime = 0;
frameDesc.LayerCount = 1;
frameDesc.Layers = layers;
 
vrapi_SubmitFrame2( app->GetOvrMobile(), &frameDesc );
  
DoLoad();

// stop the animation    
jni->CallStaticVoidMethod(loadingScreenClass, stopUpdatingLoadingScreenMethodID);
		
// delete local refs
jni->DeleteLocalRef( loadingScreenClass );
  
vrapi_DestroyTextureSwapChain(swapChain); 

The java method can then retrieve a Canvas from the Surface and use is methods to draw to the surface as frequently as you need to. There are no thread requirements for the java function, so you can run it on any underutilized core. Here is an example:

package com.oculus.loadingscreen;
  
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.SurfaceTexture;
import android.os.Bundle;
import android.util.Log;
import android.content.Intent;
import android.view.Surface;
  
public class LoadingScreen 
{
  
	public static final String TAG = "LoadingScreen";
  
	private static boolean shouldUpdateLoadingScreen = false;

	public static void startUpdatingLoadingScreen( final Surface surface, final int width, final int height )
{

		shouldUpdateLoadingScreen = true;
new Thread( new Runnable()
		{
		@Override
		public void run()
		{
                    Paint whitePaint = new Paint();
                    whitePaint.setColor( Color.WHITE );
                    whitePaint.setStrokeWidth( 20 );
                    whitePaint.setAntiAlias( true );
                    whitePaint.setStrokeCap( Paint.Cap.ROUND );
                    whitePaint.setStyle( Paint.Style.STROKE );
                    final int margin = 15;
                    int i = 0;
										
				while(shouldUpdateLoadingScreen)
				{
					try
					{
						Canvas canvas = surface.lockCanvas( null );
                        
						// Draw the loading screen, eg, sprite etc.
                        
						canvas.drawColor( Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
                        
						int a = (i * 2) % 360;
						int b = (i * 3) % 720;  
						b = b > 360 ? 720 - b : b;
  
						canvas.drawArc( margin, margin, width - margin, height - margin, -a, -b, false, whitePaint );
  
						surface.unlockCanvasAndPost( canvas );
  
						Thread.sleep( 16, 0 );
					} catch ( Exception e )
					{
						e.printStackTrace();
						break;
					}
					i++;
				}
			}
		} ).start();
	}

	public static void stopUpdatingLoadingScreen()
	{
		shouldUpdateLoadingScreen = false;
	}
}

Then you just need to call stopUpdatingLoadingScreen() when you are done loading to stop the java function from updating.

Similarly, it is possible to achieve the same effect in Unity, as of Oculus Integration 1.29. Take the above java code and compile it into an aar library, and place this library in Assets/Plugins/Android. Then you simply need to create an empty loading scene with a single quad object. Then attach the following script:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
  
public class LoadingScreen : MonoBehaviour {
  
    private OVROverlay overlay;
  
    public int Width = 512;
    public int Height = 512;
    public string SceneName = "";
  
    private void Awake()
    {
      overlay = GetComponent<OVROverlay>();
  
      if(overlay == null)
      {
      	overlay = gameObject.AddComponent<OVROverlay>();
      }
      overlay.isExternalSurface = true;
      overlay.externalSurfaceHeight = Width;
      overlay.externalSurfaceWidth = Height;
  
      gameObject.hideFlags = HideFlags.HideAndDontSave;
      DontDestroyOnLoad(gameObject);
    }
  
  
    private IEnumerator Start()
    {
      // Wait for surface to be available.
      while (overlay.externalSurfaceObject == System.IntPtr.Zero)
      {
      	yield return null;
      }
      // get our android object
      var loadingScreenClass = AndroidJNI.FindClass("com/oculus/loadingscreen/LoadingScreen");
      var startUpdatingLoadingScreenMethodID = AndroidJNI.GetStaticMethodID(loadingScreenClass, "startUpdatingLoadingScreen", "(Landroid/view/Surface;II)V");
      var stopUpdatingLoadingScreenMethodID = AndroidJNI.GetStaticMethodID(loadingScreenClass, "stopUpdatingLoadingScreen", "()V");
  
      AndroidJNI.CallStaticVoidMethod(loadingScreenClass, startUpdatingLoadingScreenMethodID, new jvalue[] { new jvalue { l = overlay.externalSurfaceObject }, new jvalue { i = overlay.externalSurfaceWidth }, new jvalue { i = overlay.externalSurfaceHeight } });
  
      DoLoad();
  
      AndroidJNI.CallStaticVoidMethod(loadingScreenClass, stopUpdatingLoadingScreenMethodID, new jvalue[0]);
  
      AndroidJNI.DeleteLocalRef(loadingScreenClass);
  
      Destroy(gameObject);
	}
  
	void DoLoad()
	{
			UnityEngine.SceneManagement.SceneManager.LoadScene(SceneName);   
}
  

Sample code provided herein is subject to the Oculus Examples License at https://developer.oculus.com/licenses/examples-license-1.0/