VRプラットフォームコンプライアンスノート





この記事では、複数のVRプラットフォーム用のエンターテイメントアプリケーションの開発に使用した小さなソリューションを紹介します。 アプリケーション自体はビデオプレーヤーです(360 / 4K、FullHD / 2D、2K / 3スクリーン、ビデオストリームのストリーミング、オフライン再生用のファイルへのアップロード)。 アプリケーションメニューは、特定の映画の様式化された3Dシーンで作成されます。 3画面は、ビデオが3つの画面で構成されるBarcoエスケープ形式です。



プラットフォーム用のアプリケーションを開発する必要がありました。





試験装置:





最適化



Rift Virtual Reality Check(VRC)ガイドラインMobile Virtual Reality Check(VRC)ガイドラインは、最も要求の厳しいハードウェア要件として最適です。 Oculusでレビューを行った場合、他のプラットフォームでのパフォーマンスに関する質問はありません。 アプリケーションはGearVRで60 fps、Riftで90 fpsのパフォーマンスで45分間安定して動作するはずです。 まず、自己記述式のフレームカウンターを固定して、銃の下に表示することができます。



Fpscounter
using UnityEngine; using UnityEngine.UI; public class FPSCounter : MonoBehaviour { public float updateInterval = 0.5F; public string tOut; public Text text; private float accum = 0f; // FPS accumulated over the interval private int frames = 0; // Frames drawn over the interval private float timeleft = 0f; // Left time for current interval private void Start() { timeleft = updateInterval; } private void Update() { timeleft -= Time.deltaTime; accum += Time.timeScale / Time.deltaTime; ++frames; if (timeleft <= 0.0) { float fps = accum / frames; tOut = string.Format("{0:F0} FPS", fps); timeleft = updateInterval; accum = 0f; frames = 0; } if (text != null) { text.text = tOut; } } }
      
      







しかし、 Oculus Debug Toolを使用するほうがよいでしょう。そこでは、統計が取得され、チャート上のアプリケーションの上にリアルタイムで描画されます。ユーティリティは、モバイルデバイスとデスクトップの両方に存在します







標準ルールを順守して、適切なパフォーマンスを達成することができました。









ステージで2Dビデオを再生すると、画面に「投影」され、そこからグローが生成されます。







これを行うには、特別なシェーダーを作成する必要がありました。 マテリアルには、標準の拡散テクスチャ、「太陽」からの照明のテクスチャ、「画面」からの照明のテクスチャ、照明ミキシングパラメータ、および画面の平均色(ビデオフレームの縮小テクスチャ)があります。 再生の開始時には、日光から画面まで滑らかに混合され、各ビデオフレームは1色に圧縮され、照明がペイントされます。







シェーダーカラーライトマップ
 Shader "Onix/Unlit/ColorLightmap" { Properties { _Diffuse("Diffuse", 2D) = "white" {} [HideInInspector] _texcoord( "", 2D ) = "white" {} _LightmapWhite("LightmapWhite", 2D) = "white" {} [HideInInspector] _texcoord2( "", 2D ) = "white" {} _LightmapDark("LightmapDark", 2D) = "white" {} _LightColor("LightColor", 2D) = "white" {} _LightValue("LightValue", Range( 0 , 1)) = 0 } SubShader { Tags { "RenderType"="Opaque" "LightMode" = "ForwardBase" } LOD 100 Cull Off Pass { CGPROGRAM #pragma target 3.0 #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float4 texcoord : TEXCOORD0; float4 texcoord1 : TEXCOORD1; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct v2f { float4 vertex : SV_POSITION; float4 texcoord : TEXCOORD0; float4 lightColor : COLOR; UNITY_VERTEX_OUTPUT_STEREO }; uniform sampler2D _Diffuse; uniform float4 _Diffuse_ST; uniform sampler2D _LightmapWhite; uniform float4 _LightmapWhite_ST; uniform sampler2D _LightmapDark; uniform float4 _LightmapDark_ST; uniform sampler2D _LightColor; uniform float _LightValue; v2f vert ( appdata v ) { v2f o; UNITY_SETUP_INSTANCE_ID(v); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); o.texcoord.xy = v.texcoord.xy; o.texcoord.zw = v.texcoord1.xy; // ase common template code o.vertex.xyz += float3(0,0,0) ; o.vertex = UnityObjectToClipPos(v.vertex); o.lightColor = tex2Dlod(_LightColor, float4(0.5, 0.5, 0, 16.0)); return o; } fixed4 frag (v2f i ) : SV_Target { fixed4 myColorVar; // ase common template code float2 uv_Diffuse = i.texcoord.xy * _Diffuse_ST.xy + _Diffuse_ST.zw; float2 uv2_LightmapWhite = i.texcoord.zw * _LightmapWhite_ST.xy + _LightmapWhite_ST.zw; float2 uv2_LightmapDark = i.texcoord.zw * _LightmapDark_ST.xy + _LightmapDark_ST.zw; float4 lerpResult4 = lerp( tex2D( _LightmapWhite, uv2_LightmapWhite ) , ( tex2D( _LightmapDark, uv2_LightmapDark ) * i.lightColor) , _LightValue); float4 blendOpSrc10 = tex2D( _Diffuse, uv_Diffuse ); float4 blendOpDest10 = lerpResult4; myColorVar = ( saturate( ( blendOpDest10 > 0.5 ? ( 1.0 - ( 1.0 - 2.0 * ( blendOpDest10 - 0.5 ) ) * ( 1.0 - blendOpSrc10 ) ) : ( 2.0 * blendOpDest10 * blendOpSrc10 ) ) )); return myColorVar; } ENDCG } } CustomEditor "ASEMaterialInspector" }
      
      







フレームからのテクスチャは、RenderTextureとGraphics.Blitを使用して取得およびスケーリングするのが最も高速です。



フレームスケーリング
 Texture texture = subPlayer.TextureProducer.GetTexture(); if (texture != null) { if (_videoFrame == null) { _videoFrame = new RenderTexture(32, 32, 0, RenderTextureFormat.ARGB32); _videoFrame.useMipMap = true; _videoFrame.autoGenerateMips = true; _videoFrame.Create(); } _videoFrame.DiscardContents(); // blit to RT so we can average over some pixels Graphics.Blit(texture, _videoFrame); }
      
      







スムージング



画像がまだ「ピクセル化」されている場合-アンチエイリアスを適用する必要がある場合は、ミップマップを有効にします。 サポートされているデバイスに対してResolutionScaleを使用することもできます。



解像度スケール
 if (OVRPlugin.tiledMultiResSupported) { UnityEngine.XR.XRSettings.eyeTextureResolutionScale = 1.2f; OVRPlugin.tiledMultiResLevel = OVRPlugin.TiledMultiResLevel.LMSMedium; } else { UnityEngine.XR.XRSettings.eyeTextureResolutionScale = 1.1f; }
      
      







UIのフォントのアンチエイリアスには、シェーダーを使用できます。



シェーダーAAFont
 Shader "UI/AAFont" { Properties { [PerRendererData] _MainTex("Sprite Texture", 2D) = "white" {} _Color("Tint", Color) = (1,1,1,1) _StencilComp("Stencil Comparison", Float) = 8 _Stencil("Stencil ID", Float) = 0 _StencilOp("Stencil Operation", Float) = 0 _StencilWriteMask("Stencil Write Mask", Float) = 255 _StencilReadMask("Stencil Read Mask", Float) = 255 _ColorMask("Color Mask", Float) = 15 [Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip("Use Alpha Clip", Float) = 0 } SubShader { Tags { "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent" "PreviewType" = "Plane" "CanUseSpriteAtlas" = "True" } Stencil { Ref[_Stencil] Comp[_StencilComp] Pass[_StencilOp] ReadMask[_StencilReadMask] WriteMask[_StencilWriteMask] } Cull Off Lighting Off ZWrite Off ZTest[unity_GUIZTestMode] Blend SrcAlpha OneMinusSrcAlpha ColorMask[_ColorMask] Pass { Name "Default" CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma target 2.0 #include "UnityCG.cginc" #include "UnityUI.cginc" #pragma multi_compile __ UNITY_UI_CLIP_RECT #pragma multi_compile __ UNITY_UI_ALPHACLIP struct appdata_t { float4 vertex : POSITION; float4 color : COLOR; float2 texcoord : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct v2f { float4 vertex : SV_POSITION; fixed4 color : COLOR; float2 texcoord : TEXCOORD0; float4 worldPosition : TEXCOORD1; UNITY_VERTEX_OUTPUT_STEREO }; fixed4 _Color; fixed4 _TextureSampleAdd; float4 _ClipRect; v2f vert(appdata_t v) { v2f OUT; UNITY_SETUP_INSTANCE_ID(v); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT); OUT.worldPosition = v.vertex; OUT.vertex = UnityObjectToClipPos(OUT.worldPosition); OUT.texcoord = v.texcoord; OUT.color = v.color * _Color; return OUT; } sampler2D _MainTex; fixed4 frag(v2f IN) : SV_Target { float2 dx = ddx(IN.texcoord) * 0.25; float2 dy = ddy(IN.texcoord) * 0.25; float4 tex0 = tex2D(_MainTex, IN.texcoord + dx + dy); float4 tex1 = tex2D(_MainTex, IN.texcoord + dx - dy); float4 tex2 = tex2D(_MainTex, IN.texcoord - dx + dy); float4 tex3 = tex2D(_MainTex, IN.texcoord - dx - dy); float4 tex = (tex0 + tex1 + tex2 + tex3) * 0.25; half4 color = (tex + _TextureSampleAdd) * IN.color; #ifdef UNITY_UI_CLIP_RECT color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect); #endif #ifdef UNITY_UI_ALPHACLIP clip(color.a - 0.001); #endif return color; } ENDCG } } }
      
      







ユーザーインターフェース



CurvedUIプラグインは、さまざまなVRプラットフォームで入力方法をすばやく切り替える優れた仕事をします。また、VRのキャンバスをより湾曲した形状に曲げることができます(ダイナミックバッチ処理を壊すのは残念です)。







ビデオを再生する



少し前まで、UnityはVideo Playerの多くの欠点を改善し、修正しました。 使いやすさ、さまざまなプラットフォームとの互換性、パフォーマンスは向上していますが、現在のアプリケーションで使用するには不十分です(大容量ファイルのサポート、パフォーマンス、コーデックサポート、ビデオストリーミングの問題のため)。 多くの異なるソリューションを試した後AVProVideoに決めました 。 彼は、モバイルデバイスとデスクトップソリューションの両方で適切なパフォーマンスを確保できる唯一の人物です。



公開する前に、必ずハードウェアデコードをインストールしてください。 Oculus Riftを構築するには、Use Unity Audioをインストールし、オーディオ出力コンポーネントを追加する必要があります-これにより、ユーザーはデフォルトのWindowsデバイスではなくVRヘッドセットでオーディオを聞くことができます(一部のユーザーは非標準構成を持っている場合があります)。







360ビデオは、ユーザーの周りの球、3プレーンメッシュの3画面で再生されます。







ファイルをアップロードする



WebClientと、実際には標準的な方法を使用してファイルをアップロードできます。



ダウンロードファイル
 private void DownloadFile() { ServicePointManager.ServerCertificateValidationCallback = MyRemoteCertificateValidationCallback; _webClient = new WebClient(); _webClient.DownloadFileCompleted += new System.ComponentModel.AsyncCompletedEventHandler(AsyncCallDownloadComplete); _webClient.DownloadProgressChanged += new DownloadProgressChangedEventHandler(AsyncCallDownloadProgress); _webClient.DownloadFileAsync(new Uri(videoData.path), _lclPath); _isDownloading = true; } private void AsyncCallDownloadComplete(object sender, System.ComponentModel.AsyncCompletedEventArgs e) { _isDownloading = false; if (e.Error == null) { _dwnloadProgress = 1f; } else { _dwnloadProgress = 0f; File.Delete(_lclPath); Debug.Log(e.Error.Message); } } private void AsyncCallDownloadProgress(object sender, DownloadProgressChangedEventArgs e) { _dwnloadProgress = (float)e.ProgressPercentage / 100f; if (e.ProgressPercentage == 100) { _isDownloading = false; _dwnloadProgress = 1f; } }
      
      







さらに、証明書を確認する必要がある場合があります。



リモート証明書検証
 using System.Net; using System.Net.Security; using System.Security.Cryptography.X509Certificates; //To validate SSL certificates public static bool MyRemoteCertificateValidationCallback(System.Object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { bool isOk = true; // If there are errors in the certificate chain, look at each error to determine the cause. if (sslPolicyErrors != SslPolicyErrors.None) { for (int i = 0; i < chain.ChainStatus.Length; i++) { if (chain.ChainStatus[i].Status != X509ChainStatusFlags.RevocationStatusUnknown) { chain.ChainPolicy.RevocationFlag = X509RevocationFlag.EntireChain; chain.ChainPolicy.RevocationMode = X509RevocationMode.Online; chain.ChainPolicy.UrlRetrievalTimeout = new TimeSpan(0, 1, 0); chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllFlags; bool chainIsValid = chain.Build((X509Certificate2)certificate); if (!chainIsValid) { isOk = false; } } } } return isOk; }
      
      







パスに沿って保存できます:



データパス
 _lclPath = Application.persistentDataPath + filename;
      
      







注釈



そして、VRプラットフォームでアプリケーションを移植または開発するときに必要になる可能性のある小さなメモとソリューションのリスト。 エディターに既に含まれているVR用の標準プラグインのみを使用したかったのですが、結局のところ、これは不可能です。 プラットフォームの要件を満たすには、プラットフォームのプラグインを統合していくつかの利点を実装する必要があります。



オクルス。 アプリケーションはReCenter機能をサポートする必要があります。



最近
 private void Update() { if (OVRPlugin.shouldRecenter) { ReCenter(); } } private void ReCenter() { UnityEngine.XR.InputTracking.Recenter(); }
      
      







GearVR。 アプリケーションは、アプリケーションを終了するためのメニューを提供する必要があります。



終了を確認
 //OVRManager.PlatformUIConfirmQuit(); //or OVRPlugin.ShowUI(OVRPlugin.PlatformUI.ConfirmQuit);
      
      







スペースでヘルメットの位置を無効にする必要がある場合は、このスクリプトを使用できます。 注意! プラットフォームの要件と矛盾します。



VRMoveHack
 using UnityEngine; public class VRMoveHack : MonoBehaviour { public Transform vrroot; private Transform mTransform; private Vector3 initialPosition; private void Awake() { mTransform = GetComponent<Transform>(); initialPosition = mTransform.position; //Disabling position breaks the head model on Gear VR, and is against the Rift store guidelines. //I would recommend instead of disabling positional tracking, to give a bubble on rift of ~1m that if the player leaves the screen fades to black. //If you have to disable positional tracking, at least add back in the head model, which consists of a position shift of 0.075f * camera up + 0.0805f * camera forward. initialPosition += (mTransform.up * 0.075f) + (mTransform.forward * 0.0805f); } private void LateUpdate() { Vector3 vrpos; vrpos = mTransform.TransformPoint(vrroot.localPosition); vrpos = mTransform.InverseTransformPoint(vrpos); mTransform.position = initialPosition - vrpos; } }
      
      







デイドリーム Daydreamプラットフォームの場合、戻るアイコンをクリックするとアプリケーションが終了します。すべてのプラットフォームで戻るボタンを使用できるスクリプトを次に示します。



エスケープ/戻る
 using UnityEngine; public class EscapeScene : MonoBehaviour { public static System.Action OnEscape; private void Update() { #if MYGOOGLEVR if (Input.GetKeyDown(KeyCode.Escape)) Application.Quit(); if (GvrControllerInput.AppButtonDown) #else if (Input.GetKeyDown(KeyCode.Escape) || Input.GetButtonDown("Cancel") || Input.GetKeyDown(KeyCode.JoystickButton2)) #endif { if (OnEscape != null) OnEscape.Invoke(); } } }
      
      







GearVR。 アプリケーションは最新のOSバージョンでテストされないため、拒否される場合があります。 Mininum APIレベルでビルド:Android 5.0(APIレベル21)、そうでない場合は、OSの最新バージョンを搭載したデバイスでアプリケーションをテストするために必要なレビューへのコメントを書き込みます。



GearVR /リフト。 VRC.Mobile.Securityが原因で理解できない障害が発生しないようにするには、リリース前にSTOREだけでなく、ALPHAおよびRCチャンネルにアセンブリをアップロードします。



iOS サードパーティのコンテンツを使用する場合は、その使用に関するドキュメント/契約書を添付することを忘れないでください。 テキストとスクリーンショットを使用して、段ボールシステムの使用方法、アプリ内アプリケーションとアプリケーションのテスト場所を説明した「経験豊富な」レビュアーに連絡できました。



Viveport。 プラットフォームがDRMを有効にするためにSDKを統合することが望ましいです。 自動WrapperベースのDRMを使用できますが、これは.Netフレームワークで実装されたものを使用していない場合のみです。 アプリケーションがテストに合格しない場合は終了しました。



蒸気 ストアにはエンターテイメントアプリケーション用のセクションが含まれていないため、ビデオセクションまたはソフトウェアセクションでビデオコンテンツを個別に公開することをお勧めします。 これが、おそらくMaskiアプリケーションがSoftware / Video Productionセクションにある理由です。



特徴 「特集」のサイトリクエストを行った場合。 アプリケーションをさらに詳しく調べます。 ヘルプを削除する(Google)、ユーザーインターフェイスのボタンにアイコンを追加する(Google)、さらにスムージングする(Oculus)、いくつかの環境要素を動的にする(たとえば、波(Oculus))ように求められる場合があります。



Oculus:私のプロジェクト/コードを確認してください。 Oculusでは、エンジニアにプロジェクトのレビューを依頼する機会があります。 あなたはソースコードを提供し、数日であなたは変更、最適化のヒント、改善を伴うプロジェクトを手に入れます。 それらの一部は一般的なものであり、一部は特定のものであり、Oculusプラットフォームでのみ実装できます。



必要に応じて、インターネットアクセスがあるかどうかを確認し、ユーザーにメッセージを表示する必要があります。 エラーに関する情報を表示するか、シーンまたはコンテンツを長時間ロードする場合(「ロード中」、「バッファリング」、「インターネット接続を確認して再試行してください」など)を表示する必要があります。 そうしないと、アプリケーションに障害があると見なされます。



インターネット到達可能性
 if (Application.internetReachability == NetworkReachability.NotReachable)
      
      







VRヘルメットを削除すると、アプリケーションが一時停止します。 このイベントをキャッチするには、標準の方法を使用できます。



アプリケーションの一時停止
 private void OnApplicationPause(bool pause) { //do pause }
      
      








All Articles