🐊
部屋の天井が開いて仮想空間が見えるMR演出を作る【Unity6×Meta XR SDK】
作ったもの
部屋の天井が開いて仮想空間が見えるやつ。
準備
環境
- Windows 11
- Unity 6(6000.0.26f1)
- URP 17.0.3
必要アセット
- Meta XR Core SDK(74.0.2)
- Meta MR Utility Kit(74.0.2)
- DOTween(1.2.765)
- 天井が開くアニメーションのため
全体像
- MRUKのEffectMesh越しでのみ仮想空間を描画する
- ステンシルを利用
- ステンシルの適用範囲を操作できるようにして開閉を表現
手順
1. MRシーン作り
- BuildingBlocksからCameraRigとPassthroughを配置。
- OVRCameraRigのOVRManager設定
- Scene Support->Supported or Required
- Passthrough Support->Supported or Required
- Enable Passthroughにチェック
2. 仮想空間の作成
今回仮想空間として、球体の内側の面に宇宙のパノラマテクスチャを貼り付けて天球を作ります。
- Sphereオブジェクトを作成して部屋の大きさを覆うのに十分なスケールに拡大する
- マテリアルのShaderはUnlit、Render FaceはBackにします。こうすることで球体の内側にテクスチャが描画され、プラネタリウムのようになります。
3. MRUKで天井を認識 & メッシュ化
- MRUKプレハブ(Packages/com.meta.xr.mrutilitykit/Core/Tools/MRUK.prefab)をシーンに配置
- EffectMeshプレハブ(Packages/com.meta.xr.mrutilitykit/Core/Tools/EffectMesh.prefab)をシーンに配置
- EffectMeshのLabelsをCEILINGに変更
ここまでで実際に実行してみると、仮想空間とEffectMeshのデフォルトマテリアルのみ表示されます。
4. EffectMesh用のステンシルによるマスクシェーダーを作成
こちらは仮想空間をマスクするためのシェーダーです。このEffectMesh越しにのみ仮想空間が表示されるようにします。またステンシルが適用される範囲を、XYZ軸方向に、0~1の値で調整できるようになります。
適当なシェーダーを作成し、以下のコードを貼り付けます。
Shader "Custom/StencilPassthrough"
{
Properties
{
_WorldMin ("World Min (Start of Range)", Vector) = (0,0,0,0)
_WorldRange ("World Size (X,Y,Z)", Vector) = (1,1,1,0)
_CutoffX ("Cutoff X (0~1)", Float) = 1
_CutoffY ("Cutoff Y (0~1)", Float) = 1
_CutoffZ ("Cutoff Z (0~1)", Float) = 1
}
SubShader
{
Tags
{
"Queue" = "Geometry-1" "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline"
}
Pass
{
Stencil
{
Ref 1
Comp Always
Pass Replace
}
ColorMask 0 // 色は描画しない(ステンシルのみ)
ZWrite Off
Cull Off // 両面描画(必要なら)
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
struct Attributes
{
float4 positionOS : POSITION;
};
struct Varyings
{
float4 positionHCS : SV_POSITION;
float3 worldPos : TEXCOORD0;
};
float3 _WorldMin;
float3 _WorldRange;
float _CutoffX;
float _CutoffY;
float _CutoffZ;
Varyings vert(Attributes IN)
{
Varyings OUT;
float3 worldPos = TransformObjectToWorld(IN.positionOS.xyz);
OUT.positionHCS = TransformWorldToHClip(worldPos);
OUT.worldPos = worldPos;
return OUT;
}
half4 frag(Varyings IN) : SV_Target
{
float3 relPos = IN.worldPos - _WorldMin;
float3 normPos = relPos / max(_WorldRange, 1e-4); // 正規化 0〜1
if (normPos.x > _CutoffX || normPos.y > _CutoffY || normPos.z > _CutoffZ)
discard; // 範囲外は破棄(ステンシルも書かれない)
return half4(0, 0, 0, 0); // ステンシルだけ書く
}
ENDHLSL
}
}
}
※開閉機能がいらない場合はプロパティ部分とdiscardしている部分が不要になります。
※ルームの端の位置とルームの大きさが必要なので、後からスクリプトから渡せるようにします。
5. 天球描画用シェーダーを作成
こちらは天球に適用するシェーダーで、URPのUnlitシェーダーにStencil部分を追加しただけです。
Shader "Custom/StencilSkySphere"
{
Properties
{
_MainTex ("Sky Texture", 2D) = "white" {}
}
SubShader
{
Tags { "Queue" = "Background" "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" }
Pass
{
Stencil
{
Ref 1
Comp Equal // ステンシルが1の場所だけ天球を描画
Pass Keep
}
Cull Off // 裏面も描画(カメラが球の中にいるため)
ZWrite Off // 深度書き込みなし
Blend SrcAlpha OneMinusSrcAlpha
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
struct Attributes
{
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
};
struct Varyings
{
float4 positionHCS : SV_POSITION;
float2 uv : TEXCOORD0;
};
sampler2D _MainTex;
float4 _MainTex_ST;
Varyings vert(Attributes IN)
{
Varyings OUT;
OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
OUT.uv = TRANSFORM_TEX(IN.uv, _MainTex);
return OUT;
}
half4 frag(Varyings IN) : SV_Target
{
return tex2D(_MainTex, IN.uv);
}
ENDHLSL
}
}
}
6. 作成したシェーダーを適用する
- まずはEffectMeshのMesh materialに4で作成したシェーダーを適用したマテリアルを追加します。
- 次に天球に5で作成したシェーダーを適用したマテリアルを追加します。
7. ステンシルの範囲を動かせるようにする
最後に、値を変更して徐々に仮想空間が見えるようにする機能を作ります。
以下のスクリプトをEffectMeshにアタッチします。詳細はコード内のコメントを参照してください。
using System.Collections.Generic;
using DG.Tweening;
using Meta.XR.MRUtilityKit;
using UnityEngine;
public class EffectMeshMaterialController : MonoBehaviour
{
[SerializeField, Range(0f, 1f)]
private float _cutoffX;
[SerializeField, Range(0f, 1f)]
private float _cutoffY;
[SerializeField, Range(0f, 1f)]
private float _cutoffZ;
private EffectMesh _effectMesh;
public enum Axis
{
X,
Y,
Z
}
private readonly Dictionary<Axis, string> _axisToPropertyName = new()
{
{Axis.X, "_CutoffX"},
{Axis.Y, "_CutoffY"},
{Axis.Z, "_CutoffZ"}
};
private void Start()
{
_effectMesh = GetComponent<EffectMesh>();
MRUK.Instance.RoomCreatedEvent.AddListener(ReceiveCreatedRoom);
}
// Roomが作成されたときに呼ばれるコールバック
private void ReceiveCreatedRoom(MRUKRoom room)
{
var mat = _effectMesh.MeshMaterial;
var bounds = room.GetRoomBounds();
mat.SetVector("_WorldMin", bounds.min); // 範囲の始点
mat.SetVector("_WorldRange", bounds.size * 1.01f); // 1.01fをかけないとルームの境目で表示が不安定になる
mat.SetFloat("_CutoffX", _cutoffX);
mat.SetFloat("_CutoffY", _cutoffY);
mat.SetFloat("_CutoffZ", _cutoffZ);
}
// fromからto(0~1の間)にduration秒かけて、Cutoffの値を徐々に変更する
private void AnimateCutoff(Axis axis, float from, float to, float duration)
{
DOVirtual.Float(from, to, duration, value =>
{
UpdateCutoff(axis, value);
});
}
// Cutoffする値をマテリアルに適用する
private void UpdateCutoff(Axis axis, float value)
{
switch (axis)
{
case Axis.X:
_cutoffX = value;
break;
case Axis.Y:
_cutoffY = value;
break;
case Axis.Z:
_cutoffZ = value;
break;
}
_effectMesh.MeshMaterial.SetFloat(_axisToPropertyName[axis], value);
}
// 以下はデバッグ用
private void Update()
{
if (OVRInput.GetDown(OVRInput.RawButton.A))
{
UpdateCutoff(Axis.Z, 1);
AnimateCutoff(Axis.X, from:0, to:1, duration:3);
}
if (OVRInput.GetDown(OVRInput.RawButton.B))
{
UpdateCutoff(Axis.Z, 1);
AnimateCutoff(Axis.X, from:1, to:0, duration:3);
}
}
#if UNITY_EDITOR
void OnValidate()
{
if(_effectMesh == null) return;
var mat = _effectMesh.MeshMaterial;
mat.SetFloat("_CutoffX", _cutoffX);
mat.SetFloat("_CutoffY", _cutoffY);
mat.SetFloat("_CutoffZ", _cutoffZ);
}
#endif
}
完成!
Quest3のAボタンで天井が開いて、Bボタンで天井が閉まります。
Simulatorの場合はキーボードのBとNです。
改善点、今後できそうなこと
- 起動時のHMDの向きによって開閉の向きが変わってしまう
- Tracking Origin TypeをStageにすればいい?
- 壁の位置関係から計算?
- 開閉時の見た目をシェーダーでリッチにする
参考
Discussion