この記事はIwakenLab. Advent Calendar 2025の3日目の記事です!
昨日はToLpazさんの 「自己紹介:『異世界クリエイター』として生きる」 でした!
今回は、Quest3の両目カメラを使って奥行のある立体的な映像を作る方法についてお話します。
今回作るもの
こちらが今回の記事で作るものです。
Quest3のカメラ映像を板ポリに映しているのですが、そこにひと手間加えることで板ポリに映る映像をより立体的にします。
動画だと分かりづらいですが、実機でみると奥行を感じることが出来、結構立体的に見えます。
以下のリポジトリのAssets\Kenty\Scenes\PCA-Stereoscopic3Dのシーンをビルドして、是非、実機で確認してみてください!
概要説明
今回行うのは「ステレオスコピック」と呼ばれるものです。
人間は 左目と右目で少しだけ違う位置から世界を見ています。その「わずかなズレ(視差)」を脳が合成することで、人間は奥行きがある立体の世界を認識出来ているわけです。
ステレオスコピックもそれと同じで、視差のある映像を左目と右目それぞれに見せることで、ただの画像でも奥行きを感じられるようにしています。
スマホVRなどが分かりやすい例で、スマホVRではSBS(Side By Side)と呼ばれる視差のある映像を仕切りで区切り、左目と右目に見せています。
今回はMetaの新機能を使ってSBSの映像を作り、それを左右それぞれの目に映すことで立体的な映像を作ることを目指します。
動作環境
- Quest3
- Windows 11
- Unity2022.3.22f1
- Oculus XR Plugin v4.2.0
- Meta XR Core SDK v81.0.0
- Meta MR Utility Kit v81.0.0
- 最近のアップデートにより同時に左右両方のカメラ映像が取得出来るようになりました。今回はその機能を活用します。
- Meta XR Interaction SDK v81.0.0
- Meta XR Interaction SDK Essentials v81.0.0
- Meta XR Simulator v81.0.0
プロジェクトのセットアップ
Unityを開いて上記のパッケージをインストールし、Meta XR ToolsのProject Setup Toolを使ってエラーを解消します。
Unity-PassthroughCameraApiSamplesのリポジトリからZipファイルを取得します。
Zipを展開しUnity-PassthroughCameraApiSamples-main\Unity-PassthroughCameraApiSamples-main\AssetsにあるPassthroughCameraApiSamplesのフォルダをUnityにドラッグ&ドロップします。
Assets\PassthroughCameraApiSamplesにあるMultiObjectDetectionは使わないのでフォルダごと削除します。
PassthroughCameraApiSamplesのシーンがちゃんと機能するか確認するため、CameraViewerのシーンを開き、Project Setup Toolのエラーを修正します。
エラーを修正したらビルドして、カメラアクセスがちゃんと出来ていることを確認しましょう。
上記まででQuest3のカメラ映像を取得することは確認できたので、ここから本題に入っていきます。
方針としては、カメラ映像を横にならべてSBSの映像を作り、それを板ポリに映します。板ポリに映す際、Shaderで映像を分割して左目・右目にそれぞれ違う映像を見せることで立体的に見せるという感じです。
SBS(Side By Side)の映像を作る
新しくシーンを作り、以下のBuildingBlockを追加します。
- Camera Rig
- Passthrough
- Hand Tracking
- InteractionRig
- Grab Intaraction
映像を映した板ポリを手で動かしたいので、Grab IntaractionのCubeをQuadに変えて、サイズや位置を調整します。
次に、空のオブジェクトを2つ作ってそれぞれにPassthroughCameraAccessのコンポーネントを追加します。この時に、PassthroughCameraAccessのCameraPositionはそれぞれLeftとRightにして下さい。
CreateSBSTexture.csを作り、空オブジェクトにコンポーネントとして追加します。
using Meta.XR;
using System.Collections;
using UnityEngine;
public class CreateSBSTexture : MonoBehaviour
{
[SerializeField] private PassthroughCameraAccess _leftCameraAccess;
[SerializeField] private PassthroughCameraAccess _rightCameraAccess;
[SerializeField] private Material _stereoTargetMaterial;
private RenderTexture _stereoTexture;
private static readonly int MainTexId = Shader.PropertyToID("_MainTex");
private void OnEnable()
{
StartCoroutine(EnsureStreams());
}
private IEnumerator EnsureStreams()
{
while (!OVRPermissionsRequester.IsPermissionGranted(OVRPermissionsRequester.Permission.PassthroughCameraAccess))
{
yield return null;
}
StartCoroutine(StreamEye(_leftCameraAccess, _rightCameraAccess));
}
private IEnumerator StreamEye(PassthroughCameraAccess accessL, PassthroughCameraAccess accessR)
{
if (!accessL || !accessR || !_stereoTargetMaterial)
{
yield break;
}
while (isActiveAndEnabled)
{
if (!accessL.IsPlaying || !accessR.IsPlaying)
{
yield return null;
continue;
}
var leftTexture = accessL.GetTexture();
var rightTexture = accessR.GetTexture();
if (!leftTexture || !rightTexture)
{
yield return null;
continue;
}
EnsureStereoTarget(leftTexture.width, leftTexture.height, rightTexture.width, rightTexture.height);
Graphics.SetRenderTarget(_stereoTexture);
GL.PushMatrix();
GL.LoadPixelMatrix(0, _stereoTexture.width, 0, _stereoTexture.height);
GL.Clear(false, true, Color.clear);
Graphics.DrawTexture(new Rect(0, 0, leftTexture.width, leftTexture.height), leftTexture);
Graphics.DrawTexture(new Rect(leftTexture.width, 0, rightTexture.width, rightTexture.height), rightTexture);
GL.PopMatrix();
Graphics.SetRenderTarget(null);
_stereoTargetMaterial.SetTexture(MainTexId, _stereoTexture);
yield return null;
}
}
private void EnsureStereoTarget(int leftW, int leftH, int rightW, int rightH)
{
int targetWidth = leftW + rightW;
int targetHeight = Mathf.Max(leftH, rightH);
if (_stereoTexture && (_stereoTexture.width != targetWidth || _stereoTexture.height != targetHeight))
{
_stereoTexture.Release();
Destroy(_stereoTexture);
_stereoTexture = null;
}
if (!_stereoTexture)
{
_stereoTexture = new RenderTexture(targetWidth, targetHeight, 0, RenderTextureFormat.ARGB32)
{
wrapMode = TextureWrapMode.Clamp,
filterMode = FilterMode.Bilinear
};
_stereoTexture.Create();
}
}
private void OnDisable()
{
if (_stereoTexture)
{
_stereoTexture.Release();
Destroy(_stereoTexture);
_stereoTexture = null;
}
}
}
このコードでは、PassthroughCameraAccessでカメラ映像をテクスチャとして取得し、それを横方向に結合したレンダーテクスチャを作成しています。その後、作成したテクスチャをマテリアルの_MainTexプロパティにセットしています。
CreateSBSTextureのコンポーネント追加後、LeftCameraAccessとRightCameraAccessに先ほどの作ったPassthroughCameraAccessコンポーネントを持ったオブジェクトをアタッチします。
次にUnlit/Textureのマテリアルを作成し、そのマテリアルをQuadとCreateSBSTextureのStereoTargetMaterialにアタッチします。
PlayしてシーンのQuadが以下のように見えていれば、SBSの映像は完成です。
映像を分割して左目と右目に分けて見せる
SBSSplit.shaderのマテリアルを作成します。
Shader "Custom/SBSSplit"
{
Properties
{
_MainTex("SBSTexture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_instancing
#include "UnityCG.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
UNITY_VERTEX_OUTPUT_STEREO
};
v2f vert(appdata v)
{
v2f o;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_INITIALIZE_OUTPUT(v2f, o);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(i);
float2 uv = i.uv;
uv.y = 1 - uv.y;
uv.x = uv.x * 0.5 + unity_StereoEyeIndex * 0.5;
fixed4 col = tex2D(_MainTex, uv);
return col;
}
ENDCG
}
}
}
このShaderでは、appdataやv2f構造体にUNITY_VERTEX_INPUT_INSTANCE_IDやUNITY_VERTEX_OUTPUT_STEREOなどのマクロを追加することで、左目と右目に別々の映像を見せることをしています。
特に重要なのがunity_StereoEyeIndexで、これは左目のレンダリングの時は0を、右目のレンダリングの時には1を返してくれます。
左目にはSBS映像の左側、つまりuv.xが0~0.5の所が必要で、右目にはSBS映像の右側、つまりuv.xが0.5~1の所が必要になります。
そのためuv.x = uv.x * 0.5 + unity_StereoEyeIndex * 0.5と書くことで、左右の目で違うものを見せることが出来ます。
マテリアル作成後、QuadとCreateSBSTextureのStereoTargetMaterialにアタッチします。
シーンを保存してBuildすると完成です!
最後に
如何でしたか?これを機に、Quest3のカメラアクセスについて興味を持っていただけると幸いです!
明日はOtoriffさんの記事になります。乞うご期待!













