すぎしーのXRと3DCG

主にXR, Unity, 3DCG系の記事を投稿していきます。

【Unity, XR】左右のビューで異なるテクスチャをサンプリングするシェーダー (unity_StereoEyeIndex使用)

f:id:tsgcpp:20200623010202j:plain

概要

1つのシェーダーで左右異なるテクスチャを描画する方法を紹介します。
例えばXR向けの鏡を作りたい場合、左右それぞれで鏡の映像をRenderTextureに描画して、左右のビューそれぞれに反映する必要があります。
今回はその様に左右の目で異なるテクスチャをサンプリングするシェーダーの実装方法を紹介します。

XR向けポストプロセッシングシェーダーとの違いも紹介していますので、併せてどうぞ。

今回もブログの最後にサンプルプロジェクトを置いておきます。

実行環境

今回作るシェーダーの特徴

  • 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なら右目向け

使用する左右の画像

以下の画像を左右のビューそれぞれに描画します。
f:id:tsgcpp:20200623013441p:plain  f:id:tsgcpp:20200623013425p:plain

スクリプト実装のポイント

要点のみを記載しますので、最終的な実装状態はサンプルプロジェクトを参照してください。

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に直接描画する場合

この記事の目的とは少しずれるので詳細は割愛しますが、 GraphicsCommandBufferを使用します。

// 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_TEX2DARRAYunity_StereoEyeIndexを渡して左右のビューそれぞれのテクスチャをサンプリング
    • unity_StereoEyeIndexが0なら左目、1なら右目

_MainTexをTex2DArrayとして宣言

  • UNITY_DECLARE_TEX2DARRAY(_MainTex)を記載して、Tex2DArrayとして宣言

XR向けポストプロセッシングシェーダーとの実装の違い

XR向けポストプロセッシングシェーダー(以下PPS)と今回作成するシェーダー(以下SES)は
実装や構成がかなり似ているため、主な違いについて紹介します。

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を指定する必要有り

SESのシェーダーについての余談

コンパイルされたシェーダーがSinglePassInstancedと同様の構成なるため、
今回作成するSESはSinglePassInstancedとの相性が良かったりします。

実行結果

問題なければ左右のビューで異なるテクスチャが描画されているはずです。

f:id:tsgcpp:20200623010202j:plain

サンプルプロジェクト

github.com

雑感

このテクニックを使えばXR向けのどこでもドアのようなワープゲートや鏡なども、シェーダーでエフェクトを付けることが可能になります。

実は、どっちかというとTex2DArray型のRenderTextureにどうやって描画するかのほうがもっと重要な問題だったりします。 Camera.targetTextureにTex2DArray型は指定しても動かなかったり、UniversalRPやHDRPではCommandBufferが使えなかったりと何かと不便があったり。。。

ただ、このシェーダーの原理をわかっておくだけで、XRにおける1歩進んだ表現が可能になると思います。

話は少しずれますが、VRChatのVRC_MirrorReflectionで使うカスタムシェーダーもunity_StereoEyeIndexを使用する点においては原理は同じです。
(VRChatはSinglePassだったり、カスタムシェーダーも_ReflectionTex0, 1で左右の鏡テクスチャを受け取るので、シェーダーの構成自体は異なります)。

最後まで読んでいただいてありがとうございました!
何かのお役に立てれば幸いです。