Viewfinder传送门

复刻viewfinder中传送门丝滑切换场景效果

Github源码传送门————>Teleporter_ViewFinder

Bilibili教程传送门————>小祥带你实现Viewfinder传送门转场效果

实机

1.平行场景渲染

viewfinder中传送门丝滑切换场景的效果,相当于通过传送门屏幕观察一个『平行世界』,当我们启动传送门后玩家位置没有变化,但周围的场景发生变化,给玩家一种时空穿梭的感觉,就达到我们的目的。

从实机画面可以看到,传送门画面是会跟随玩家的移动产生变化的说明是实时渲染,那么毫无疑问下一个关卡的场景是已经加载好的,通过一个相机渲染到传送门屏幕上。另外还可以观察到,当玩家站在原地时仅移动玩家相机,传送门画面并没有发生变化,那么可以推测渲染场景的相机仅仅跟随玩家移动,但并不会跟随玩家相机旋转,其一直朝向传送门屏幕中心点。

1
2
3
4
5
6
7
8
private void Update()
{
if (needLookAt)
{
teleporterCamera.transform.position = playerCamera.transform.position;
teleporterCamera.transform.LookAt(teleporterCanvas.transform);
}
}

2.转场画面对齐

传送门屏幕上的画面是渲染在世界空间的面片上的,那么该如何让渲染在屏幕上的画面和场景相机的画面在转场的时候完全对齐,注意是完全对齐,因为一旦出现任何偏差都会导致严重的割裂感,所以对齐画面是整个效果最重要的一环。

unity中相机渲染出的画面可以理解为视锥体内所有物体透视投影到近裁剪平面的结果,那么要对齐画面就有思路了,我们只要将传送门上渲染场景画面的面片,放在相机的近裁剪平面处并调整大小使之完全保持一致即可。当然实际应用中,传送门是固定不动的,我们移动玩家和相机来对齐画面。另外玩家作为游戏中最不可预料的对象,触发转场时剥夺玩家控制权也是同样重要的XD

1
2
3
4
Vector3 canvasPos = teleporterCanvas.transform.position;
playerController.transform.DOMove(canvasPos + teleporterCanvas.transform.forward * 1f, 2f);
playerController.transform.DODynamicLookAt(canvasPos, 2f);
playerCamera.transform.DODynamicLookAt(canvasPos, 2f).OnComplete(OnMovePlayerCameraCompleted);
  • 这里直接使用RawImage渲染场景画面,当然其Canvas需要设置为WorldSpace,获取到面片位置之后让玩家和相机一起移动到Canvas正面。
  • canvasPos + teleporterCanvas.transform.forward * 1f再Canvas坐标基础上再添加一个Canvas的方向向量,是为了确保相机旋转到距离Canvas正面一定距离防止传送门穿模,同时也让画面比例比较合理,方便后续转场动画。
  • 因为场景渲染相机是一直跟随玩家位置移动,且他本来就是看向面片的,所以这里只需要移动玩家位置,让玩家相机看向面片即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void OnMovePlayerCameraCompleted()
{
needLookAt = false;
teleporterCanvas.renderMode = RenderMode.ScreenSpaceOverlay;
teleporterCanvas.worldCamera = playerCamera;
RectTransform rectTransform = teleporterCanvas.GetComponent<RectTransform>();
rectTransform.position = Vector3.zero;
teleporterRawImage.transform.localScale = Vector3.one;
RectTransform maskRectTransform = teleporterCanvas.transform.GetChild(0).GetComponent<RectTransform>();
maskRectTransform.DOSizeDelta(new Vector2(3000f, 3000f), 2f)
.OnComplete(() =>
{
maskRectTransform.sizeDelta = Vector2.zero;
StartCoroutine(SwitchScene());
});
}
  • 移动完玩家和相机之后这个时候其实画面已经对齐了,转场动画实际上是RawImage上的圆形遮罩不断放大直到覆盖住整个画面,然后关掉RawImage让玩家相机的画面显示出来就可以完美衔接画面。
  • Canvas再这个阶段需要转到ScreenSpaceOverlay模式,因为如果继续在世界空间下放大遮罩会导致穿模。
1
2
3
4
5
6
7
8
9
10
11
12
IEnumerator SwitchScene()
{
if (!unloadSceneName.Equals("_Main"))
{
playerCamera.cullingMask = (1 << LayerMask.NameToLayer("Default")) | (1 << LayerMask.NameToLayer(loadSceneName));

Physics.IgnoreLayerCollision(LayerMask.NameToLayer("Player"), LayerMask.NameToLayer(loadSceneName), false);
Physics.IgnoreLayerCollision(LayerMask.NameToLayer("Player"), LayerMask.NameToLayer(unloadSceneName), true);
playerController.enabled = true;
yield return SceneManager.UnloadSceneAsync(unloadSceneName);
}
}
  • 转场结束后修改相机的剔除层级和物理的碰撞层级确保相机渲染和场景玩家碰撞正确,同时开启玩家控制并卸载旧场景,整个流程就算完成。

3.Canvas尺寸计算

前面提到『将传送门上渲染场景画面的面片,放在相机的近裁剪平面处并调整大小使之完全保持一致即可』,那么具体该怎么计算Canvas的尺寸呢,下面给出=计算过程:

  • d = 1
  • FOV = 60°
  • Near = 0.3
  • Width:Height = 1920:1080

计算公式:
$$
\text{World Height} = 2 \times d \times \tan\left(\frac{FOV}{2}\right)
$$

$$
\text{World Width} = \text{World Height} \times \frac{1920}{1080}
$$

代入数值:
$$
\text{World Height} = 2 \times 1 \times \tan\left(\frac{60^\circ}{2}\right) = 2 \times 1 \times \tan(30^\circ) \approx 1.1547
$$

$$
\text{World Width} = \text{World Height} \times \frac{1920}{1080} \approx 1.1547 \times 1.7777 \approx 2.054
$$

因此:

  • World Height ≈ 1.1547 单位
  • World Width ≈ 2.054 单位

Canvas 的原始尺寸是 1920×1080 像素,因此计算缩放比例如下:

$$
\text{Scale}_X = \frac{\text{World Width}}{1920} = \frac{2.054}{1920} \approx 0.00107
$$

$$
\text{Scale}_Y = \frac{\text{World Height}}{1080} = \frac{1.1547}{1080} \approx 0.00107
$$

Canvas 距离相机为 1 单位 时,所需的 缩放比例 为:

$$
\text{Scale} = (0.00107, 0.00107, 1)
$$

4.可优化方向

  • 因为需要提前加载下一个场景到内存中,所以如果传送门比较多的话需要加载很多的场景,像游戏中的场景选择关卡,性能开销太大显然不可能全部加载,我个人理解是在传送门进入相机视野时加载对应场景,离开视野后卸载,所以游戏中的传送门很少有集中放在一起的,都是分散摆放。还有一种可能就是提前加载的场景是特殊处理的,只放通过传送门能看到的部分,但我感觉这个太麻烦了,还是前面那个方法靠谱点。
  • 碰撞和相机的剔除都是通过改变Layer实现的,这要求场景内所有物体都统一为同一个层级,后续如果有射线检测之类的功能就很麻烦。
  • 还遇到一个问题,在代码中动态生成renderTexture赋值给RawImage(Canvas为WorldSpaceMode)时图像会沿x轴发生镜像,但Canvas切换回ScreenSpaceOverlay模式又恢复正常,这个还需要研究一下什么原因造成的,如果有小伙伴找到原因的话请务必Email告诉我orz

完结撒花~

pid:128242791


Viewfinder传送门
https://baifabaiquan.cn/2025/03/15/Teleporter_ViewFinder/
作者
白发败犬
发布于
2025年3月15日
许可协议