//======= Copyright (c) Valve Corporation, All rights reserved. =============== // // Purpose: Helper for smoothing over transitions between levels. // //============================================================================= using UnityEngine; using System.Collections; using Valve.VR; using System.IO; namespace Valve.VR { public class SteamVR_LoadLevel : MonoBehaviour { private static SteamVR_LoadLevel _active = null; public static bool loading { get { return _active != null; } } public static float progress { get { return (_active != null && _active.async != null) ? _active.async.progress : 0.0f; } } public static Texture progressTexture { get { return (_active != null) ? _active.renderTexture : null; } } // Name of level to load. public string levelName; // Name of internal process to launch (instead of levelName). public string internalProcessPath; // The command-line args for the internal process to launch. public string internalProcessArgs; // If true, call LoadLevelAdditiveAsync instead of LoadLevelAsync. public bool loadAdditive; // Async load causes crashes in some apps. public bool loadAsync = true; // Optional logo texture. public Texture loadingScreen; // Optional progress bar textures. public Texture progressBarEmpty, progressBarFull; // Sizes of overlays. public float loadingScreenWidthInMeters = 6.0f; public float progressBarWidthInMeters = 3.0f; // If specified, the loading screen will be positioned in the player's view this far away. public float loadingScreenDistance = 0.0f; // Optional overrides for where to display loading screen and progress bar overlays. // Otherwise defaults to using this object's transform. public Transform loadingScreenTransform, progressBarTransform; // Optional skybox override textures. public Texture front, back, left, right, top, bottom; // Colors to use when dropping to the compositor between levels if no skybox is set. public Color backgroundColor = Color.black; // If false, the background color above gets applied as the foreground color in the compositor. // This does not have any effect when using a skybox instead. public bool showGrid = false; // Time to fade from current scene to the compositor and back. public float fadeOutTime = 0.5f; public float fadeInTime = 0.5f; // Additional time to wait after finished loading before we start fading the new scene back in. // This is to cover up any initial hitching that takes place right at the start of levels. // Most scenes should hopefully not require this. public float postLoadSettleTime = 0.0f; // Time to fade loading screen in and out (also used for progress bar). public float loadingScreenFadeInTime = 1.0f; public float loadingScreenFadeOutTime = 0.25f; float fadeRate = 1.0f; float alpha = 0.0f; AsyncOperation async; // used to track level load progress RenderTexture renderTexture; // used to render progress bar ulong loadingScreenOverlayHandle = OpenVR.k_ulOverlayHandleInvalid; ulong progressBarOverlayHandle = OpenVR.k_ulOverlayHandleInvalid; public bool autoTriggerOnEnable = false; void OnEnable() { if (autoTriggerOnEnable) Trigger(); } public void Trigger() { if (!loading && !string.IsNullOrEmpty(levelName)) StartCoroutine(LoadLevel()); } // Helper function to quickly and simply load a level from script. public static void Begin(string levelName, bool showGrid = false, float fadeOutTime = 0.5f, float r = 0.0f, float g = 0.0f, float b = 0.0f, float a = 1.0f) { var loader = new GameObject("loader").AddComponent(); loader.levelName = levelName; loader.showGrid = showGrid; loader.fadeOutTime = fadeOutTime; loader.backgroundColor = new Color(r, g, b, a); loader.Trigger(); } // Updates progress bar. void OnGUI() { if (_active != this) return; // Optionally create an overlay for our progress bar to use, separate from the loading screen. if (progressBarEmpty != null && progressBarFull != null) { if (progressBarOverlayHandle == OpenVR.k_ulOverlayHandleInvalid) progressBarOverlayHandle = GetOverlayHandle("progressBar", progressBarTransform != null ? progressBarTransform : transform, progressBarWidthInMeters); if (progressBarOverlayHandle != OpenVR.k_ulOverlayHandleInvalid) { var progress = (async != null) ? async.progress : 0.0f; // Use the full bar size for everything. var w = progressBarFull.width; var h = progressBarFull.height; // Create a separate render texture so we can composite the full image on top of the empty one. if (renderTexture == null) { renderTexture = new RenderTexture(w, h, 0); renderTexture.Create(); } var prevActive = RenderTexture.active; RenderTexture.active = renderTexture; if (Event.current.type == EventType.Repaint) GL.Clear(false, true, Color.clear); GUILayout.BeginArea(new Rect(0, 0, w, h)); GUI.DrawTexture(new Rect(0, 0, w, h), progressBarEmpty); // Reveal the full bar texture based on progress. GUI.DrawTextureWithTexCoords(new Rect(0, 0, progress * w, h), progressBarFull, new Rect(0.0f, 0.0f, progress, 1.0f)); GUILayout.EndArea(); RenderTexture.active = prevActive; // Texture needs to be set every frame after it is updated since SteamVR makes a copy internally to a shared texture. var overlay = OpenVR.Overlay; if (overlay != null) { var texture = new Texture_t(); texture.handle = renderTexture.GetNativeTexturePtr(); texture.eType = SteamVR.instance.textureType; texture.eColorSpace = EColorSpace.Auto; overlay.SetOverlayTexture(progressBarOverlayHandle, ref texture); } } } #if false // Draw loading screen and progress bar to 2d companion window as well. if (loadingScreen != null) { var screenAspect = (float)Screen.width / Screen.height; var textureAspect = (float)loadingScreen.width / loadingScreen.height; float w, h; if (screenAspect < textureAspect) { // Clamp horizontally w = Screen.width * 0.9f; h = w / textureAspect; } else { // Clamp vertically h = Screen.height * 0.9f; w = h * textureAspect; } GUILayout.BeginArea(new Rect(0, 0, Screen.width, Screen.height)); var x = Screen.width / 2 - w / 2; var y = Screen.height / 2 - h / 2; GUI.DrawTexture(new Rect(x, y, w, h), loadingScreen); GUILayout.EndArea(); } if (renderTexture != null) { var x = Screen.width / 2 - renderTexture.width / 2; var y = Screen.height * 0.9f - renderTexture.height; GUI.DrawTexture(new Rect(x, y, renderTexture.width, renderTexture.height), renderTexture); } #endif } // Fade our overlays in/out over time. void Update() { if (_active != this) return; alpha = Mathf.Clamp01(alpha + fadeRate * Time.deltaTime); var overlay = OpenVR.Overlay; if (overlay != null) { if (loadingScreenOverlayHandle != OpenVR.k_ulOverlayHandleInvalid) overlay.SetOverlayAlpha(loadingScreenOverlayHandle, alpha); if (progressBarOverlayHandle != OpenVR.k_ulOverlayHandleInvalid) overlay.SetOverlayAlpha(progressBarOverlayHandle, alpha); } } // Corourtine to handle all the steps across loading boundaries. IEnumerator LoadLevel() { // Optionally rotate loading screen transform around the camera into view. // We assume here that the loading screen is already facing toward the origin, // and that the progress bar transform (if any) is a child and will follow along. if (loadingScreen != null && loadingScreenDistance > 0.0f) { Transform hmd = this.transform; if (Camera.main != null) hmd = Camera.main.transform; Quaternion rot = Quaternion.Euler(0.0f, hmd.eulerAngles.y, 0.0f); Vector3 pos = hmd.position + (rot * new Vector3(0.0f, 0.0f, loadingScreenDistance)); var t = loadingScreenTransform != null ? loadingScreenTransform : transform; t.position = pos; t.rotation = rot; } _active = this; SteamVR_Events.Loading.Send(true); // Calculate rate for fading in loading screen and progress bar. if (loadingScreenFadeInTime > 0.0f) { fadeRate = 1.0f / loadingScreenFadeInTime; } else { alpha = 1.0f; } var overlay = OpenVR.Overlay; // Optionally create our loading screen overlay. if (loadingScreen != null && overlay != null) { loadingScreenOverlayHandle = GetOverlayHandle("loadingScreen", loadingScreenTransform != null ? loadingScreenTransform : transform, loadingScreenWidthInMeters); if (loadingScreenOverlayHandle != OpenVR.k_ulOverlayHandleInvalid) { var texture = new Texture_t(); texture.handle = loadingScreen.GetNativeTexturePtr(); texture.eType = SteamVR.instance.textureType; texture.eColorSpace = EColorSpace.Auto; overlay.SetOverlayTexture(loadingScreenOverlayHandle, ref texture); } } bool fadedForeground = false; // Fade out to compositor SteamVR_Events.LoadingFadeOut.Send(fadeOutTime); // Optionally set a skybox to use as a backdrop in the compositor. var compositor = OpenVR.Compositor; if (compositor != null) { if (front != null) { SteamVR_Skybox.SetOverride(front, back, left, right, top, bottom); // Explicitly fade to the compositor since loading will cause us to stop rendering. compositor.FadeGrid(fadeOutTime, true); yield return new WaitForSeconds(fadeOutTime); } else if (backgroundColor != Color.clear) { // Otherwise, use the specified background color. if (showGrid) { // Set compositor background color immediately, and start fading to it. compositor.FadeToColor(0.0f, backgroundColor.r, backgroundColor.g, backgroundColor.b, backgroundColor.a, true); compositor.FadeGrid(fadeOutTime, true); yield return new WaitForSeconds(fadeOutTime); } else { // Fade the foreground color in (which will blend on top of the scene), and then cut to the compositor. compositor.FadeToColor(fadeOutTime, backgroundColor.r, backgroundColor.g, backgroundColor.b, backgroundColor.a, false); yield return new WaitForSeconds(fadeOutTime + 0.1f); compositor.FadeGrid(0.0f, true); fadedForeground = true; } } } // Now that we're fully faded out, we can stop submitting frames to the compositor. SteamVR_Render.pauseRendering = true; // Continue waiting for the overlays to fully fade in before continuing. while (alpha < 1.0f) yield return null; // Keep us from getting destroyed when loading the new level, otherwise this coroutine will get stopped prematurely. transform.parent = null; DontDestroyOnLoad(gameObject); if (!string.IsNullOrEmpty(internalProcessPath)) { Debug.Log("[SteamVR] Launching external application..."); var applications = OpenVR.Applications; if (applications == null) { Debug.Log("[SteamVR] Failed to get OpenVR.Applications interface!"); } else { var workingDirectory = Directory.GetCurrentDirectory(); var fullPath = Path.Combine(workingDirectory, internalProcessPath); Debug.Log("[SteamVR] LaunchingInternalProcess"); Debug.Log("[SteamVR] ExternalAppPath = " + internalProcessPath); Debug.Log("[SteamVR] FullPath = " + fullPath); Debug.Log("[SteamVR] ExternalAppArgs = " + internalProcessArgs); Debug.Log("[SteamVR] WorkingDirectory = " + workingDirectory); var error = applications.LaunchInternalProcess(fullPath, internalProcessArgs, workingDirectory); Debug.Log("[SteamVR] LaunchInternalProcessError: " + error); #if UNITY_EDITOR UnityEditor.EditorApplication.isPlaying = false; #elif !UNITY_METRO System.Diagnostics.Process.GetCurrentProcess().Kill(); #endif } } else { var mode = loadAdditive ? UnityEngine.SceneManagement.LoadSceneMode.Additive : UnityEngine.SceneManagement.LoadSceneMode.Single; if (loadAsync) { Application.backgroundLoadingPriority = ThreadPriority.Low; async = UnityEngine.SceneManagement.SceneManager.LoadSceneAsync(levelName, mode); // Performing this in a while loop instead seems to help smooth things out. //yield return async; while (!async.isDone) { yield return null; } } else { UnityEngine.SceneManagement.SceneManager.LoadScene(levelName, mode); } } yield return null; System.GC.Collect(); yield return null; Shader.WarmupAllShaders(); // Optionally wait a short period of time after loading everything back in, but before we start rendering again // in order to give everything a change to settle down to avoid any hitching at the start of the new level. yield return new WaitForSeconds(postLoadSettleTime); SteamVR_Render.pauseRendering = false; // Fade out loading screen. if (loadingScreenFadeOutTime > 0.0f) { fadeRate = -1.0f / loadingScreenFadeOutTime; } else { alpha = 0.0f; } // Fade out to compositor SteamVR_Events.LoadingFadeIn.Send(fadeInTime); // Refresh compositor reference since loading scenes might have invalidated it. compositor = OpenVR.Compositor; if (compositor != null) { // Fade out foreground color if necessary. if (fadedForeground) { compositor.FadeGrid(0.0f, false); compositor.FadeToColor(fadeInTime, 0.0f, 0.0f, 0.0f, 0.0f, false); yield return new WaitForSeconds(fadeInTime); } else { // Fade scene back in, and reset skybox once no longer visible. compositor.FadeGrid(fadeInTime, false); yield return new WaitForSeconds(fadeInTime); if (front != null) { SteamVR_Skybox.ClearOverride(); } } } // Finally, stick around long enough for our overlays to fully fade out. while (alpha > 0.0f) yield return null; if (overlay != null) { if (progressBarOverlayHandle != OpenVR.k_ulOverlayHandleInvalid) overlay.HideOverlay(progressBarOverlayHandle); if (loadingScreenOverlayHandle != OpenVR.k_ulOverlayHandleInvalid) overlay.HideOverlay(loadingScreenOverlayHandle); } Destroy(gameObject); _active = null; SteamVR_Events.Loading.Send(false); } // Helper to create (or reuse if possible) each of our different overlay types. ulong GetOverlayHandle(string overlayName, Transform transform, float widthInMeters = 1.0f) { ulong handle = OpenVR.k_ulOverlayHandleInvalid; var overlay = OpenVR.Overlay; if (overlay == null) return handle; var key = SteamVR_Overlay.key + "." + overlayName; var error = overlay.FindOverlay(key, ref handle); if (error != EVROverlayError.None) error = overlay.CreateOverlay(key, overlayName, ref handle); if (error == EVROverlayError.None) { overlay.ShowOverlay(handle); overlay.SetOverlayAlpha(handle, alpha); overlay.SetOverlayWidthInMeters(handle, widthInMeters); // D3D textures are upside-down in Unity to match OpenGL. if (SteamVR.instance.textureType == ETextureType.DirectX) { var textureBounds = new VRTextureBounds_t(); textureBounds.uMin = 0; textureBounds.vMin = 1; textureBounds.uMax = 1; textureBounds.vMax = 0; overlay.SetOverlayTextureBounds(handle, ref textureBounds); } // Convert from world space to tracking space using the top-most camera. var vrcam = (loadingScreenDistance == 0.0f) ? SteamVR_Render.Top() : null; if (vrcam != null && vrcam.origin != null) { var offset = new SteamVR_Utils.RigidTransform(vrcam.origin, transform); offset.pos.x /= vrcam.origin.localScale.x; offset.pos.y /= vrcam.origin.localScale.y; offset.pos.z /= vrcam.origin.localScale.z; var t = offset.ToHmdMatrix34(); overlay.SetOverlayTransformAbsolute(handle, SteamVR.settings.trackingSpace, ref t); } else { var t = new SteamVR_Utils.RigidTransform(transform).ToHmdMatrix34(); overlay.SetOverlayTransformAbsolute(handle, SteamVR.settings.trackingSpace, ref t); } } return handle; } } }