文章目录
- 📕教程说明
- [📕Shader 实现](#📕Shader 实现)
- 📕控制物体之间的渲染顺序和深度
- 📕将现实的门窗替换成虚拟的门窗
- 📕解决一些小问题
此教程相关的详细教案,文档,思维导图和工程文件会放入 Spatial XR 社区 。这是一个高质量 XR 开发者社区,博主目前在内担任 XR 开发的讲师。该社区提供专人答疑、完整进阶教程、从零到一项目孵化保姆服务(包含产品上架App lab)、投资|融资对接、工程文件下载等服务。
社区链接:
SpatialXR社区:完整课程、项目下载、项目孵化宣发、答疑、投融资、专属圈子
📕教程说明
这期教程我将介绍如何实现 MR 门窗效果。我会教大家如何把现实的门窗替换成虚拟的门窗,实现虚拟场景与现实空间的融合,也就是局部透视的效果(如下图所示),那么我们就可以透过虚拟的门窗看到位于现实房间之外的虚拟世界。
学了本期以及即将推出的几期教程,你将了解大部分 MR 游戏或应用中在现实空间中透视出虚拟场景的实现原理。
配套的视频链接:
https://www.bilibili.com/video/BV1TK421h7Me
系列教程专栏:https://blog.csdn.net/qq_46044366/category_12118293.html
Meta XR SDK 版本:v63
Unity 版本:2022.3.20f1
📕Shader 实现
我们主要会用一种 Shader 来实现我们想要达到的视觉效果。我们可以直接使用 Meta 的一个开源项目 "The World Beyond" 中写好的一个 Shader。
项目链接:https://github.com/oculus-samples/Unity-TheWorldBeyond/tree/main
csharp
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* Licensed under the Oculus SDK License Agreement (the "License");
* you may not use the Oculus SDK except in compliance with the License,
* which is provided at the time of installation or download, or which
* otherwise accompanies this software in either electronic or hard copy form.
*
* You may obtain a copy of the License at
*
* https://developer.oculus.com/licenses/oculussdk/
*
* Unless required by applicable law or agreed to in writing, the Oculus SDK
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Shader "TheWorldBeyond/DepthOnly" {
Properties {
}
SubShader {
Tags{"RenderType" = "Transparent"} LOD 100
// First Pass: render outside shell of hand, as depth object
Pass {
ColorMask 0 Blend SrcAlpha OneMinusSrcAlpha CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata {
float4 vertex : POSITION;
};
struct v2f {
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
v2f vert(appdata v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
UNITY_TRANSFER_FOG(o, o.vertex);
return o;
}
fixed4 frag(v2f i) : SV_Target {
//clip(mask.r - 0.5);
return float4(0,0,0,0);
}
ENDCG
}
}
}
这个 Shader 不会写入颜色信息,只会写入深度信息,所以叫 Depth Only。因此,如果一个物体的材质用上了这个 Depth Only Shader,那么我们看不到这个物体的颜色,但是该物体仍然会参与到深度测试中。根据这个特性,我们可以进一步分析。
📕控制物体之间的渲染顺序和深度
为了达到我们想要实现的效果,我们主要会涉及到三类物体:
1)现实房间外的虚拟场景
- 现实的墙(具有透视材质的平面)
3)位于墙上的虚拟门窗
我们会给位于墙上的虚拟门窗添加 Depth Only Shader,然后通过控制物体之间的渲染顺序和深度来实现 MR 门窗效果。
深度涉及到深度测试 。深度测试会基于一个像素的深度值与深度缓冲区中已有的值进行比较,来决定一个像素是否应该被绘制。深度说白了就是物体与相机的距离。一般来说深度测试的条件是小于等于,也就是说如果像素的深度值小于等于深度缓冲区已有的值,那么会通过深度测试,这个像素就会被渲染出来。如果大于,就不会被渲染。这个情况符合我们的现实规律,因为在现实中位于前面的物体能够遮挡住位于后面的物体,如果把我们的眼睛比作相机,我们先看到了位于前面的物体,然后位于后面的物体深度比前面的物体大,那么它被前面的物体挡住了,我们也就看不到位于后面的物体,就是位于后面的物体没有通过深度测试。那么放在计算机的渲染当中也是类似的,先渲染的物体会把它的深度值写入深度缓冲区,然后接下来渲染的物体会与先渲染的物体进行深度比较,假如我们想要让位于前面的物体遮挡住位于后面的物体,那么位于后面的物体深度大,不会通过深度测试,就不会被渲染出来。
渲染顺序可以通过设置 Render Queue 来控制。Render Queue 越大的物体越后渲染。
因此,我们以这样设置:
Render Queue:虚拟场景<门窗<墙 (我这里设置为虚拟场景:2000,门窗上的 Depth Only Shader:2998,墙上的 SelectivePassthrough Shader:2999)
深度:门窗<墙<虚拟场景
那么渲染顺序就是先渲染虚拟场景,再渲染门窗,再渲染墙。结合深度,我们可以这样分析:
由于虚拟世界是首先被渲染的,它的深度信息会被写入深度缓冲区。这个阶段没有先前的深度信息可以比较,所以虚拟世界的渲染不会被深度测试阻止。
接下来渲染门窗,由于 Depth Only Shader 的作用,虽然不写入颜色信息,但会更新深度缓冲区。这个更新会覆盖之前虚拟场景在相同像素位置的深度信息,因为门在这些位置上的深度值更小(更靠近相机)。
接下来渲染墙。对于没有被门遮挡的部分,墙会成功通过深度测试并覆盖虚拟场景的像素,因为这部分区域的深度缓冲区记录的是虚拟场景的深度信息,只有与门重叠的这部分的深度信息被替换成了门的深度信息,其他位置记录的还是原先虚拟场景的深度。然而,对于被门遮挡的部分,因为墙的深度值大于门在深度缓冲区中设置的值(即,墙在门之后),那么这部分墙在深度测试中会失败,因此不会在这些像素位置上覆盖之前的内容(即虚拟世界)。因为门本身没有颜色,所以门这块区域就只剩下最早保留的虚拟世界的颜色。
总结一下,Depth Only Shader 能够影响在它之后渲染的物体,如果在它之后渲染的物体深度更大,那么被它遮住的这部分不会渲染出颜色。
📕将现实的门窗替换成虚拟的门窗
首先需要配置好场景理解的功能,可以参考我之前写的这篇教程:https://blog.csdn.net/qq_46044366/article/details/135930423
需要配置具有透视材质的 Plane Prefab 和 Volume Prefab 用来表示现实中的平面和 3D 物体。
接下来我们需要识别出现实中的门窗,这个可以在 Quest 的空间设置步骤中标出现实中的门窗,然后添加上对应的标签。
然后我们可以修改 OVRSceneManager 的 Prefab Overrides,我们将标签为门(DOOR_FRAME)和窗户(WINDOW_FRAME)的现实物体替换成虚拟的门窗物体。
门:
根物体需要添加 OVRSceneAnchor 脚本,子物体需要有一个 Quad 物体,范围和门口的大小一样,该物体的材质需要是 Depth Only Shader
窗:
最终效果:
📕解决一些小问题
这时候运行程序,我们可能会遇到几个问题:
1)打开程序的一瞬间会看到完整的虚拟场景,随后现实场景才出现
2)虚拟的地面可能会比现实地面高
第一个问题出现的原因是:场景模型的加载需要一段时间,可能刚打开程序的时候场景模型还没加载好,因此这个时候看不到场景模型中的带有透视材质的房间物体。
第二个问题出现的原因是:假如虚拟场景的地面高度为 0,场景模型中的地面高度不一定是 0,如果比 0 小,那么看上去虚拟地面就会比现实地面高。
因此,解决思路是:等到场景模型加载完毕后,显示虚拟场景,并且调整虚拟场景的高度,让虚拟场景的地面略微低于场景模型中代表了现实地面的场景锚点。
我们可以写个脚本来实现:
csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class VirtualWorldManager : MonoBehaviour
{
//虚拟场景的父物体
public Transform envRoot;
//让虚拟场景位于场景模型的floor anchor之下多少米
public float groundDelta = 0.02f;
private OVRSceneManager sceneManager;
private void Awake()
{
sceneManager = GetComponent<OVRSceneManager>();
envRoot.gameObject.SetActive(false);
sceneManager.SceneModelLoadedSuccessfully += InitRoom;
}
private void InitRoom()
{
List<OVRSceneAnchor> ovrSceneAnchors = new List<OVRSceneAnchor>();
OVRSceneAnchor.GetSceneAnchors(ovrSceneAnchors);
for(int i = 0; i < ovrSceneAnchors.Count; i++)
{
OVRSceneAnchor sceneAnchor = ovrSceneAnchors[i];
OVRSemanticClassification classification = sceneAnchor.GetComponent<OVRSemanticClassification>();
if (classification != null && classification.Contains(OVRSceneManager.Classification.Floor))
{
Vector3 envPos = envRoot.transform.position;
float groundHeight = sceneAnchor.transform.position.y - groundDelta;
envRoot.transform.position = new Vector3(envPos.x, groundHeight, envPos.z);
}
}
envRoot.gameObject.SetActive(true);
}
}
将这个脚本添加到挂载了 OVRSceneManager 脚本的物体上:
EnvRoot 物体为虚拟场景的父物体。