Skip to content

Instantly share code, notes, and snippets.

@krzys-h
Last active February 15, 2021 17:34
Show Gist options
  • Save krzys-h/98464aa2f4a1ad814358f8f078111366 to your computer and use it in GitHub Desktop.
Save krzys-h/98464aa2f4a1ad814358f8f078111366 to your computer and use it in GitHub Desktop.
[Unity] A script to handle creation and registration of OpenVR manifests in a Unity-friendly way

A script to generate and register an OpenVR manifest in a Unity-friendly way

OpenVR manifest (a .vrmanifest file) lets you set a name and image for your application to be displayed in Steam/SteamVR interface, or even launch it from there. You can see it for example in the SteamVR status window (by default it shows only the process name), at the bottom of SteamVR system interface or in compositor loading screens (when your app hangs or is loading new scene, etc.)

Steam generates manifests automatically for games that you own, but you may want to have that functionality in standalone applications, and Unity doesn't seem to have any builtin support for it.

Everything this script does is based mostly on experimentation as the OpenVR documentation on manifests seems to be severly lacking.

Usage

  1. Put this script anywhere in your assets folder
  2. Click Edit > Project Settings > OpenVR Manifest
  3. Fill in the settings
  4. On build, everything will happen automatically, and manifest will be registered upon launching the app
  5. If you want, you can move the file Assets/Resources/OpenVRManifestSettings.asset wherever you wish, but it must be stored in a folder called "Resources"

Available settings

App key

This setting is REQUIRED. It is the unique key that identifies your app in the system. For example, steam games use steam.app.STEAMID and builtin tools use openvr.tool.XXXXXX

App image / app image URL

This is the app image that will get displayed in the interface. You can provide either a Texture2D, a web URL or a path to a file relative to build output dir. The recommended size is 460x215 (standard Steam game tile image size)

Launch arguments

If you want to pass any custom arguments when starting the app via Steam/SteamVR interface, you can do so here

Temporary manifest

If set to true, the manifest will be loaded only during current SteamVR session and disappear after a restart. If false, path to the manifest will be stored permanently in SteamVR configuration and loaded on start each time.

I recommend to set it to false during development.

Note: Apps from temporary manifests can't be started with Steam/SteamVR interface Note 2: If you need to remove a permanent manifest, look in /config/appconfig.json

Final notes

  • Main application name is taken from Unity project configuration. Look in Edit > Project Settings > Player
  • The script has been tested in Unity 2017.3.0f3 and was confirmed to work fine with SteamVR and VRTK assets.
// A script to generate and register an OpenVR manifest in a Unity-friendly way
// OpenVR manifest lets you show proper app name in SteamVR status screen instead of process name, set a custom image for the loading screen in compositor or even allow to launch your game from Steam interface
// Feel free to do anything you want with this script, but please keep this copyright notice intact.
// https://gist.github.com/krzys-h/98464aa2f4a1ad814358f8f078111366
// Author: krzys_h, 2018-02-11
// Usage:
// Put this script anywhere in your assets folder
// Click Edit > Project Settings > OpenVR Manifest
// Fill in the settings
// On build, everything will happen automatically!
// If you want, you can move the file Assets/Resources/OpenVRManifestSettings.asset wherever you wish, but it must be stored in a folder called "Resources"
using System;
using System.IO;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.Callbacks;
#endif
#if !UNITY_EDITOR
using Valve.VR;
#endif
public class OpenVRManifest : ScriptableObject
{
private const string MANIFEST_SETTINGS_ASSET_NAME = "OpenVRManifestSettings";
private const int APP_IMAGE_RECOMMENDED_WIDTH = 460;
private const int APP_IMAGE_RECOMMENDED_HEIGHT = 215;
[Header("App key")]
[Tooltip("App key name to use, should be unique system-wide for your application, e.g. mycompany.myapp")]
public string appKey = "";
[Header("App parameters")]
[Tooltip("The app image, used for example in loading screen while in compositor. Recommended size is 460x215")]
public Texture2D appImage = null;
[Tooltip("Alternatively, you can provide the app image as a web URL")]
public string appImageUrl = "";
[Tooltip("Launch arguments to use when launching this app via Steam/SteamVR interface")]
public string launchArguments = "";
[Header("Manifest settings")]
[Tooltip("Whether to register the manifest as temporary. Temporary manifests are loaded only until next SteamVR restart. Recommended during development as otherwise it permanently registers path to the .vrmanifest file somewhere in SteamVR. Note: Apps from temporary manifests can't be launched from Steam")]
public bool temporaryManifest = true;
// Note: if you want to remove permanently registered manifest, look in <steamdir>/config/appconfig.json
public static OpenVRManifest LoadOpenVRManifestSettings()
{
return Resources.Load<OpenVRManifest>(MANIFEST_SETTINGS_ASSET_NAME);
}
#if UNITY_EDITOR
// A menu option to open VR manifest settings
[MenuItem("Edit/Project Settings/OpenVR Manifest")]
public static void OpenSettings()
{
OpenVRManifest manifestSettingsAsset = LoadOpenVRManifestSettings();
if (manifestSettingsAsset == null)
{
if (!AssetDatabase.IsValidFolder("Assets/Resources"))
AssetDatabase.CreateFolder("Assets", "Resources");
manifestSettingsAsset = ScriptableObject.CreateInstance<OpenVRManifest>();
AssetDatabase.CreateAsset(manifestSettingsAsset, "Assets/Resources/" + MANIFEST_SETTINGS_ASSET_NAME + ".asset");
AssetDatabase.SaveAssets();
}
Selection.activeObject = manifestSettingsAsset;
}
[MenuItem("Edit/Project Settings/OpenVR Manifest", true)]
public static bool OpenSettingsValidate()
{
return Array.IndexOf(XRSettings.supportedDevices, "OpenVR") >= 0;
}
#endif
#if UNITY_EDITOR
// A class representing the structure of OpenVR manifest JSON
[Serializable]
public sealed class OpenVRManifestData
{
[Serializable]
public sealed class OpenVRManifestApplicationData
{
[Serializable]
public sealed class OpenVRManifestApplicationStringsData
{
public string name;
}
public string app_key;
public string launch_type = "binary";
public string binary_path_windows;
public string binary_path_osx; // TODO: Add support for binary_path_osx and binary_path_linux
public string binary_path_linux;
public string arguments = "";
public string arguments_osx;
public string arguments_linux;
public string image_path = ""; // can be a file name or a URL
//public Dictionary<string, OpenVRManifestApplicationStringsData> strings; // keys are language names, e.g. en_us // TODO: JSONUtility doesn't support Dictionary :/
[Serializable]
public sealed class OpenVRManifestApplicationStringsArray
{
public OpenVRManifestApplicationStringsData en_us;
}
public OpenVRManifestApplicationStringsArray strings;
//public List<string> mime_types; // For some special apps? e.g. "mime_types": [ "vr/media_player" ]
}
public string source = "builtin";
public List<OpenVRManifestApplicationData> applications;
}
// Code responsible for generating the manifest + copying the logo image in a postprocess hook
[PostProcessBuild]
public static void OnPostprocessBuild(BuildTarget target, string pathToBuiltProject)
{
if (Array.IndexOf(XRSettings.supportedDevices, "OpenVR") < 0)
return;
OpenVRManifest manifestSettingsAsset = LoadOpenVRManifestSettings();
if (manifestSettingsAsset == null)
{
Debug.Log("OpenVR manifest not configured! Click Edit > Project Settings > OpenVR Manifest");
return;
}
if (manifestSettingsAsset.appKey == "")
{
Debug.LogWarning("App key cannot be empty. OpenVR manifest will not be generated. See Edit > Project Settings > OpenVR Manifest");
return;
}
if (manifestSettingsAsset.appImage != null && manifestSettingsAsset.appImageUrl != "")
{
Debug.LogWarning("Cannot use app image and app image URL at the same time. See Edit > Project Settings > OpenVR Manifest");
throw new UnityException("Cannot use app image and app image URL at the same time");
}
string outputDirName = Path.GetDirectoryName(pathToBuiltProject);
string programBinaryName = Path.GetFileName(pathToBuiltProject);
string programBinaryNameBase = Path.GetFileNameWithoutExtension(pathToBuiltProject);
// Put the app image inside output directory
string appImage = "";
if (manifestSettingsAsset.appImageUrl != "")
{
appImage = manifestSettingsAsset.appImageUrl;
}
else if (manifestSettingsAsset.appImage)
{
if (manifestSettingsAsset.appImage.width != APP_IMAGE_RECOMMENDED_WIDTH || manifestSettingsAsset.appImage.width != APP_IMAGE_RECOMMENDED_HEIGHT)
{
Debug.LogWarning("The OpenVR app image is not using the recommended size of " + APP_IMAGE_RECOMMENDED_WIDTH + "x" + APP_IMAGE_RECOMMENDED_HEIGHT + ", but we'll use it anyway");
}
// Make sure we can access the texture with EncodeToPNG
TextureImporter importer = AssetImporter.GetAtPath(AssetDatabase.GetAssetPath(manifestSettingsAsset.appImage)) as TextureImporter;
if (importer)
{
bool dirty = false;
if (!importer.isReadable)
{
Debug.Log("Texture " + manifestSettingsAsset.appImage + " did not have script read access enabled, enabling now...");
importer.isReadable = true;
dirty = true;
}
if (importer.textureCompression != TextureImporterCompression.Uncompressed)
{
Debug.Log("Texture " + manifestSettingsAsset.appImage + " had compression enabled, disabling now...");
importer.textureCompression = TextureImporterCompression.Uncompressed;
dirty = true;
}
if (importer.npotScale != TextureImporterNPOTScale.None)
{
Debug.Log("Texture " + manifestSettingsAsset.appImage + " had NPOT scaling enabled, disabling now...");
importer.npotScale = TextureImporterNPOTScale.None;
dirty = true;
}
if (importer.mipmapEnabled)
{
Debug.Log("Texture " + manifestSettingsAsset.appImage + " had mipmaps enabled, disabling now...");
importer.mipmapEnabled = false;
dirty = true;
}
if (dirty)
{
importer.SaveAndReimport();
}
}
else
{
Debug.LogWarning("Unable to access TextureImporter for " + manifestSettingsAsset.appImage.name + " ?");
}
// And save the texture in build directory
string appImageFilename = programBinaryNameBase + "_vrlogo.png";
string appImagePath = Path.Combine(outputDirName, appImageFilename);
byte[] appImageData = manifestSettingsAsset.appImage.EncodeToPNG();
File.WriteAllBytes(appImagePath, appImageData);
appImage = appImageFilename;
}
// Generate the manifest file
OpenVRManifestData jsonData = new OpenVRManifestData()
{
applications = new List<OpenVRManifestData.OpenVRManifestApplicationData>()
{
new OpenVRManifestData.OpenVRManifestApplicationData()
{
app_key = manifestSettingsAsset.appKey,
binary_path_windows = programBinaryName,
arguments = manifestSettingsAsset.launchArguments,
image_path = appImage,
strings = new OpenVRManifestData.OpenVRManifestApplicationData.OpenVRManifestApplicationStringsArray()
{
en_us = new OpenVRManifestData.OpenVRManifestApplicationData.OpenVRManifestApplicationStringsData()
{
name = Application.productName
}
}
}
}
};
string json = JsonUtility.ToJson(jsonData, true);
string vrmanifestPath = Path.Combine(outputDirName, programBinaryNameBase + ".vrmanifest");
File.WriteAllText(vrmanifestPath, json);
}
#endif
// Code to register the manifest on runtime
// Note: Don't put the #if !UNITY_EDITOR on the outside of this block, it won't work because of a Unity bug (see https://stackoverflow.com/questions/44655667/runtimeinitializeonload-not-running-with-conditional-compilation)
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
public static void OnRuntimeLoad()
{
#if !UNITY_EDITOR
OpenVRManifest manifestSettingsAsset = LoadOpenVRManifestSettings();
if (manifestSettingsAsset == null)
{
Debug.Log("OpenVR manifest not configured!");
return;
}
// At this point, OpenVR may be already initialized (if SteamVR is enabled by default in the first scene) or not (if None is at top
// of supported SDKs list, that is the case e.g. when using VRTK, or your first scene just has no VR enabled at all).
// If it's not, we'll have to handle initialization outselves
if (XRSettings.loadedDeviceName != "OpenVR")
{
EVRInitError error = EVRInitError.None;
OpenVR.Init(ref error, EVRApplicationType.VRApplication_Utility);
if (error != EVRInitError.None)
{
Debug.LogError("Failed to initialize OpenVR to register manifest, error = " + error.ToString());
return;
}
}
string exePath = System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName; // TODO: This is UGLY! Why doesn't Unity have this
string exeDir = Path.GetDirectoryName(exePath);
string exeNameBase = Path.GetFileNameWithoutExtension(exePath);
string vrmanifestPath = Path.Combine(exeDir, exeNameBase + ".vrmanifest");
Debug.Log("Registering the OpenVR manifest from "+vrmanifestPath);
OpenVR.Applications.AddApplicationManifest(vrmanifestPath, manifestSettingsAsset.temporaryManifest);
// To make sure that the new settings get applied immediately
// (without this it works only after app restart)
// PS. OpenVR documentation sucks, it says that if you pass 0 instead of process id it will use the calling process automatically but it actually doesn't
OpenVR.Applications.IdentifyApplication((uint) System.Diagnostics.Process.GetCurrentProcess().Id, manifestSettingsAsset.appKey);
// And don't forget to clean up afterwards!
if (XRSettings.loadedDeviceName != "OpenVR")
{
OpenVR.Shutdown();
}
#endif
}
}
@jasonhbartlett
Copy link

Does this script still work and/or is relevant for the current version of OpenVR? It seems like it should be helpful, especially as my SteamVR plugin/OpenVR app is standalone but I would like to be able to launch it from within SteamVR but currently can't figure out how to do that.

@krzys-h
Copy link
Author

krzys-h commented Dec 20, 2019

@jasonhbartlett Sorry for such a late reply. The last time I used this was when I wrote it so I'm not sure it still works... but if it doesn't, what you basically want to do is to create a appname.vrmanifest file (you can look at the builtin ones somewhere in the SteamVR install directory as documentation on them was quite lacking last time I checked) and then register it using OpenVR.Applications.AddApplicationManifest(vrmanifestPath, true) (true means that the manifest is "permanent" - the path to it will be stored somewhere in SteamVR and your app will then become launchable from within SteamVR and even Steam itself iirc).

@jasonhbartlett
Copy link

@krzys-h Thank you for responding on this. I'll have to look into it and I'll be grateful for your pointing me in the right direction. Thanks so much!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment