【Unity, XR】左右のビューで異なるテクスチャをサンプリングするシェーダー (unity_StereoEyeIndex使用)
- 概要
- 実行環境
- 今回作るシェーダーの特徴
- 前知識
- 使用する左右の画像
- スクリプト実装のポイント
- シェーダー実装のポイント
- XR向けポストプロセッシングシェーダーとの実装の違い
- 実行結果
- サンプルプロジェクト
- 雑感
概要
1つのシェーダーで左右異なるテクスチャを描画する方法を紹介します。
例えばXR向けの鏡を作りたい場合、左右それぞれで鏡の映像をRenderTextureに描画して、左右のビューそれぞれに反映する必要があります。
今回はその様に左右の目で異なるテクスチャをサンプリングするシェーダーの実装方法を紹介します。
XR向けポストプロセッシングシェーダーとの違いも紹介していますので、併せてどうぞ。
今回もブログの最後にサンプルプロジェクトを置いておきます。
実行環境
- Unity 2019.4.1f1
- XR Plugin
- Mock HMD
- Oculus Quest + Oculus Link
- OpenVR Loader (SteamVR Unity Plugin v2.6.0b2 (sdk 1.12.5))
今回作るシェーダーの特徴
- XRにおいて左右のビューで異なるテクスチャをサンプリング
- 応用することで左右のビューで異なるエフェクトを作ることも可能
- RenderModeのMultiPass, SinglePassInstancedの両対応
- SinglePassはXR Pluginでは除外されているため考慮しない
前知識
ステレオカメラ
- XR有効時のCameraのこと
- 描画時に左右のビュー両方の描画を行うCamera
- 内部的に左用ビュー、右用ビューを持っておりRenderModeに従って左右の描画を行う
Tex2DArray型RenderTexture
- Editor上で作成は不可で、スクリプトから作成
- スクリプト上ではRenderTextureだが、シェーダーに渡した場合はTex2DArrayとして扱われる
- 今回は2枚のテクスチャを持つRenderTextureを作成し使用
- 0番目は左目用、1番目は右目用
Tex2DArray型RenderTextureの注意点
- Tex2DArray型RenderTextureを
Camera.targetTexture
を指定した場合の挙動は不定- UnityのバージョンやUniversalRP, HDRPなどでも挙動か変わる
- 0番目テクスチャにだけ描画されたり、まったくされなかったりする
unity_StereoEyeIndex (シェーダー内変数)
- シェーダー内で左右どちらの描画を行っているかを判定可能な変数
- 0なら左目向け、1なら右目向け
使用する左右の画像
以下の画像を左右のビューそれぞれに描画します。
スクリプト実装のポイント
要点のみを記載しますので、最終的な実装状態はサンプルプロジェクトを参照してください。
Tex2DArray型RenderTextureの作成
- ポイントはRenderTextureDescriptorを使うこと
var desc = new RenderTextureDescriptor(1024, 1024); // RenderTextureをTex2DArrayとして生成 desc.dimension = TextureDimension.Tex2DArray; // Textureは2つ(0番目は左目用、1番目は右目用) desc.volumeDepth = 2; // RenderTextureDescriptorからテクスチャを生成 var stereoTexture = RenderTexture.GetTemporary(desc);
注意
desc.vrUsage = VRTextureUsage.TwoEyes
にする場合はGraphics.Blit
が一部おかしくなるかもしれません- 詳細はこちらを参照
Tex2DArray型RenderTextureの左右それぞれに画像をコピー
Graphics.Blit
をdepthSlice付きで使用することでTex2DArray型にテクスチャをコピー可能destDepthSlice
は左目は0, 右目は1
Graphics.Blit( source: leftEyeTexture, dest: stereoTexture, sourceDepthSlice: 0, destDepthSlice: 0); Graphics.Blit( source: rightEyeTexture, dest: stereoTexture, sourceDepthSlice: 0, destDepthSlice: 1);
余談: Tex2DArray型RenderTextureに直接描画する場合
この記事の目的とは少しずれるので詳細は割愛しますが、 GraphicsやCommandBufferを使用します。
// RenderTextureの1番目を青色で塗りつぶす(テクスチャのクリア処理) Graphics.SetRenderTarget( rt: stereoTex, mipLevel: 0, face: CubemapFace.Unknown, depthSlice: 1); GL.Clear(true, true, Color.blue);
作成したRenderTextureをマテリアルの_MainTexにセット
GetComponent<Renderer>().sharedMaterial.mainTexture = stereoTexture;
シェーダー実装のポイント
- シェーダー全体はサンプルプロジェクトを参照してください。
unity_StereoEyeIndexを活用
vert (vertex shader)
UNITY_SETUP_INSTANCE_ID(v)
でunity_StereoEyeIndex
の値を取得UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output)
でfragにunity_StereoEyeIndex
を渡す
frag (fragment shader)
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input)
でvertからのinputでunity_StereoEyeIndex
を更新UNITY_SAMPLE_TEX2DARRAY
にunity_StereoEyeIndex
を渡して左右のビューそれぞれのテクスチャをサンプリングunity_StereoEyeIndex
が0なら左目、1なら右目
_MainTexをTex2DArrayとして宣言
UNITY_DECLARE_TEX2DARRAY(_MainTex)
を記載して、Tex2DArrayとして宣言
XR向けポストプロセッシングシェーダーとの実装の違い
XR向けポストプロセッシングシェーダー(以下PPS)と今回作成するシェーダー(以下SES)は
実装や構成がかなり似ているため、主な違いについて紹介します。
- 公式ドキュメント : Single Pass Instanced rendering
- XR向けポストプロセッシングについて紹介されています
PPSの場合
描画パイプラインにおけるPPS
_MainTex
は描画のたびにポストプロセッシングのシステムから渡される- MultiPassでは、
_MainTex
はTex2D型でドローコールのたびに異なる(左テクスチャ、右テクスチャ)- ※MultiPassはシェーダー1つに対して、ドローコールは2回(左右それぞれで発生)
- SinglePassInstancedでは、
_MainTex
はTex2DArray型でシェーダー内で左右のテクスチャを選別- ※SinglePassInstancedはシェーダー1つに対して、ドローコールが1回
PPSのシェーダーの特徴
上記の違いを満たすために以下のマクロを使用して、
RenderModeのMultiPass, SinglePassInstancedの両対応を実現しています。
UNITY_DECLARE_TEX2DARRAY(_MainTex)
で_MainTexの型を宣言(型の調整)- MultiPassでTex2D型、SinglePassInstancedでTex2DArray型となる
UNITY_SAMPLE_SCREENSPACE_TEXTURE
でサンプリング(_MainTexの型に合わせたサンプリング)- MultiPassでSampler2D、SinglePassInstanceでSampler2DArrayとなる
PPSのシェーダーについての余談
SinglePassInstancedの場合、コンパイルされたシェーダーがTex2DArray向けになるため、
PPSシェーダーをSESシェーダーとして再利用することが可能だったりします。
MultiPassの場合は基本的に再利用できないはずです。
SESの場合
描画パイプラインにおけるSES
_MainTex
は独自にスクリプトを用意して指定_MainTex
はTex2DArray型にしてシェーダー内でテクスチャ選別が必要- プロパティはカメラの描画毎に決定するため、左から右の描画に切り替わるときに
_MainTex
の切り替えは基本的にできない
- プロパティはカメラの描画毎に決定するため、左から右の描画に切り替わるときに
SESのシェーダーの特徴
上記の事情から基本的には_MainTex
がTex2DArray型で調整して、シェーダー内で左右を切り替えることになる。
UNITY_DECLARE_TEX2DARRAY(_MainTex)
でTex2DArray型として宣言UNITY_SAMPLE_TEX2DARRAY
でサンプリング- uv.zに
unity_StereoEyeIndex
を指定する必要有り
- uv.zに
SESのシェーダーについての余談
コンパイルされたシェーダーがSinglePassInstancedと同様の構成なるため、
今回作成するSESはSinglePassInstancedとの相性が良かったりします。
実行結果
問題なければ左右のビューで異なるテクスチャが描画されているはずです。
サンプルプロジェクト
雑感
このテクニックを使えばXR向けのどこでもドアのようなワープゲートや鏡なども、シェーダーでエフェクトを付けることが可能になります。
実は、どっちかというとTex2DArray型のRenderTextureにどうやって描画するかのほうがもっと重要な問題だったりします。 Camera.targetTextureにTex2DArray型は指定しても動かなかったり、UniversalRPやHDRPではCommandBufferが使えなかったりと何かと不便があったり。。。
ただ、このシェーダーの原理をわかっておくだけで、XRにおける1歩進んだ表現が可能になると思います。
話は少しずれますが、VRChatのVRC_MirrorReflectionで使うカスタムシェーダーもunity_StereoEyeIndexを使用する点においては原理は同じです。
(VRChatはSinglePassだったり、カスタムシェーダーも_ReflectionTex0, 1で左右の鏡テクスチャを受け取るので、シェーダーの構成自体は異なります)。
最後まで読んでいただいてありがとうございました!
何かのお役に立てれば幸いです。