Unity奇异博士传送门笔记

效果预览

项目开源地址:https://github.com/buggzd/UnityDr.StrangePortal

一些基础约定

首先玩家所在的空间叫做A空间,传送门要传送去的空间叫做B空间,同时A空间下的相机(主相机)叫camA,A空间下的传送门叫portalA,B空间同理。

实现传送门内部画面

基本思路就是使用一个RT相机(该相机的所有参数应该和A相机相同),RT相机放在B空间,叫该相机camB,对于两个空间都存在一个一模一样的传送门,我们只需要让camB相对于portalB的运动和相机A相对于portalB的运动同步,那么camB得到的rendertexture上传送门的位置和A是一模一样的,我们只需要把RT图(rendertexture)上传送门内的图像贴到portalA上,就可以得到透过传送门看到传送去的场景。


两相机视图

1. 实现相机同步

这里是一个经典的相对运动问题,和渲染管线里的MV矩阵变化过程很类似,具体的可以去看games101。把这个脚本挂到camB上,再把camA,portalA,portalB挂上运行游戏就可以看到两个相机可以同步旋转和移动了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PortalCam : MonoBehaviour
{
// Start is called before the first frame update
//camA
public Transform PlayerCamera;
//portalA
public Transform Portal;
//portalB
public Transform OtherPortal;


// Update is called once per frame
void Update()
{
//相对偏移
Vector3 OffsetFromPortal=PlayerCamera.position - OtherPortal.position;


//获取两个传送门之间的角度差值
//同步转动
float angularDifference = -Quaternion.Angle( OtherPortal.rotation, Portal.rotation);
Quaternion portalRotationDifference = Quaternion.AngleAxis(angularDifference, Vector3.up);
Vector3 newCamerDirection= portalRotationDifference * PlayerCamera.forward;
transform.rotation = Quaternion.LookRotation(newCamerDirection,Vector3.up);
//先旋转后平移
Vector3 positionOffset = Quaternion.Euler(0f, angularDifference, 0f) * OffsetFromPortal;
transform.position = positionOffset + Portal.position;

}
}

2. 实现传送门贴图

这里用到的是一个很简单的屏幕空间UV采样RT贴图。开头说明了两个相机看到的画面,传送门位置相同,所以就可以直接使用camA的屏幕空间来采样camB的rendertexture。
需要注意的是如果希望做后处理,用hdr,那么camB的rendertexture需要使用支持hdr的格式,传统的3通道8位肯定是不能记录hdr信息的,可以改成16位。

传送门屏幕空间UVshader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
Shader "Unlit/ScreenUv"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }

Pass
{

CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"


struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};

struct v2f
{

float4 screen_pos:TEXCOORD1;
float4 vertex : SV_POSITION;

};

sampler2D _MainTex;
float4 _MainTex_ST;

v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.screen_pos=o.vertex;
//平台兼容
o.screen_pos.y=o.screen_pos.y*_ProjectionParams.x;
return o;
}

fixed4 frag (v2f i) : SV_Target
{
// sample the texture
float2 uv=i.screen_pos.xy/i.screen_pos.w;
uv=(uv+1)*0.5;
fixed4 col = tex2D(_MainTex, uv);

return col;
}
ENDCG
}
}
}

使用脚本给shader赋值

这个随便写了,当然你也可以直接创建一个rt图然后拖到materia上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SetPortalTex : MonoBehaviour
{
//camB
public Camera cam;
//portalA material
public Material PortalMat;
// Start is called before the first frame update
void Start()
{
if(cam.targetTexture != null)
{
cam.targetTexture.Release();
}
//支持hdr
cam.targetTexture = new RenderTexture(Screen.width, Screen.height,24, RenderTextureFormat.RGB111110Float);

PortalMat.mainTexture = cam.targetTexture;
}


}

实现传送门粒子特效


传送门特效

传送门的粒子特效有两种实现方式,一种是使用unity自带的粒子,另一种是使用VFX。这两种方法各有各的好处,使用自带粒子是可以做墙壁碰撞的,使用VFX则需要在VFX里设置碰撞箱。使用VFX得到的效果很不错,而且很简单易上手,但是需要用URP管线。

方法一 Unity自带粒子

这里只提供思路,具体实现细节请自行实现(因为我没用原装粒子)。这里创建多个粒子生成器,每个生成器都是向上喷发粒子,然后通过脚本动画控制每个生成器的移动,让生成器绕着传送门的圆形旋转。

方法二 VFXGraph

这个方法非常简单,大致都是根据这篇文章(https://www.bilibili.com/read/cv15931018)实现的,安装一个Unity额外的VFX扩展,和shaderGraph一样的连线调数值就可以了。

实现物体传送

基础数学知识

向量点积

为了实现传送门的传送判定,我们需要知道待传送物体是否在传送门正面,此时我们就需要使用向量点积来判断。通过代码控制获取传送门正向的方向向量,计算传送门到角色的向量。
向量点积的定义:

a · b=|a||b|cos(θ)

通过定义我们可以得知,当两个向量之间夹角为-90°~90°时,点积值为正。在三维中,两向量点积为正的区域正好把空间分隔为了两半,一半是点积为正的区域,一半是点积为负的区域(当然还有0)
这里我们可以测试一下,写一个简单的shader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
Shader "Unlit/sphere"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
//方向向量
_Dir("Direction",Vector)=(1,0,0,0)
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag


#include "UnityCG.cginc"

struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};

struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 pos_OS:TEXCOORD1;
};

sampler2D _MainTex;
float4 _MainTex_ST;
float4 _Dir;

v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
//使用模型空间展示不同向量的点乘
o.pos_OS=v.vertex;

return o;
}

fixed4 frag (v2f i) : SV_Target
{

fixed4 col = tex2D(_MainTex, i.uv);
//当点积小于0的都直接舍去
clip(dot(i.pos_OS,_Dir));
return col;
}
ENDCG
}
}
}


与Direction点积为负的一半被剔除了

点积进行判断

碰撞

光有向量点积还不能实现传送门效果,因为只使用向量点积是否大于0来判断,会导致当带传送物体只要是在传送门后方都会被传送,就算是传送物体绕过了传送门也会被传送。这和我们期望的传送门效果不一样,这时就需要引入碰撞来解决这个问题。
具体的思路就是在传送门前加一个碰撞箱,当产生碰撞时再进行点积检测。


在传送面片前放置碰撞箱

更改传送物体位置

在先前同步摄像机的脚本,我们解决了相对运动的问题,这里又可以再用上了,当物体确认需要传送,只需要计算好传送物体对于PortalA的偏移值,然后加在PortalB上就可以了。需要注意的是传送需要先旋转180°,需要移动到portalB的背后而不是前面。

1
2
3
4
5
float rotationDiff=-Quaternion.Angle(Portal.rotation,OtherPortal.rotation);
rotationDiff += 180;
player.Rotate(Vector3.up,rotationDiff);
Vector3 positionOffset = Quaternion.Euler(0f, rotationDiff, 0f) * portalToPlayer;
player.position = OtherPortal.position + positionOffset;

完整源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PortalCollider : MonoBehaviour
{
//PortalA
public Transform Portal;
//PortalB
public Transform OtherPortal;

public Transform player;
private bool teleport=false;

// Start is called before the first frame update
void Start()
{

}

// Update is called once per frame

void LateUpdate()
{
if (teleport)
{
Vector3 portalToPlayer = player.position-Portal.position;

//如果走到背面点积为负数
if (Vector3.Dot(Portal.up, portalToPlayer) < 0f)
{

float rotationDiff=-Quaternion.Angle(Portal.rotation,OtherPortal.rotation);
rotationDiff += 180;
player.Rotate(Vector3.up,rotationDiff);
Vector3 positionOffset = Quaternion.Euler(0f, rotationDiff, 0f) * portalToPlayer;
player.position = OtherPortal.position + positionOffset;
Debug.Log(player.position);
teleport = false;
}
}
}

private void OnTriggerEnter(Collider other)
{
Debug.Log("OnTriggerEnter");
if (other.tag == "Player") {
teleport = true;
}
if (other.tag == "ball")
{
teleport = true;
}
}
private void OnTriggerExit(Collider other)
{
if (other.tag == "Player")
{
teleport = false;
}
if (other.tag == "ball")
{
teleport = false;
}
}
}

  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.
  • Copyrights © 2022-2023 Junto
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信