
原理介绍原理其实非常简单,就是通过调整摄像机的相对位置,将摄像机渲染出来的Target Render texture放在传送门的显示当中代码实现视觉效果根据我们说到的原理,那么我们现在就有三个摄像机player:Main Cameraindoor:portal Cameraoutdoor:portalCamera是需要三个摄像机即可实现我们想要的效果首先看一下我们传送门的结构接下来实现camera.tartgetTexture viewTexture,就是将摄像机拍摄到的画面显示在对应的门上public MyPortal linkedPortal; public MeshRenderer screen; Camera playerCam; Camera portalCam; RenderTexture viewTexture; // Start is called before the first frame update private void Awake() { playerCam Camera.main; portalCam GetComponentInChildrenCamera(); screen.material.SetInt(displayMask, 1); } // Update is called once per frame void CreateViewTexture() { if (viewTexture null || viewTexture.width ! Screen.width || viewTexture.height ! Screen.height) { if (viewTexture ! null) { viewTexture.Release(); } viewTexture new RenderTexture(Screen.width, Screen.height, 0); // Render the view from the portal camera to the view texture portalCam.targetTexture viewTexture; // Display the view texture on the screen of the linked portal linkedPortal.screen.material.SetTexture(_MainTex, viewTexture); } } public void Render() { screen.enabled false; CreateViewTexture(); var m transform.localToWorldMatrix * linkedPortal.transform.worldToLocalMatrix * playerCam.transform.localToWorldMatrix; portalCam.transform.SetPositionAndRotation(m.GetColumn(3), m.rotation); portalCam.Render(); screen.enabled true; }代码解析:我们在代码中需要引用到其他的传送门的相机,将其他的传送门上的相机渲染得到的画面显示在我们这个传送门上,对应的关键代码如下:portalCam.targetTexture viewTexture; linkedPortal.screen.material.SetTexture(_MainTex, viewTexture);这里面的SetTexture()是针对shader用的待会再解释再就是解释一下坐标的变换var m transform.localToWorldMatrix * linkedPortal.transform.worldToLocalMatrix * playerCam.transform.localToWorldMatrix; portalCam.transform.SetPositionAndRotation(m.GetColumn(3), m.rotation);在计算机图形学我们也学到了,第一个是将本地坐标转化成为世界坐标的矩阵,首先我们要先知道主相机从本地坐标转化成为世界坐标的Matrix然后用另一个传送门的worldToLocalMatrix,这样就可以将主相机的世界坐标转化为相对linkedportal的本地坐标最后用自己的传送门的localToWorldMatrix乘上上面的结果就是将主相机相对另一个传送门的本地坐标,转化成相对自己的世界坐标最后的SetpositionAndRotation就是设置相对的坐标,从而精确设置相机的位置至于为什么要先关闭screen再render()这个是因为我们的相机是从后面拍摄的,所以会导致不必要的阻塞现象,会出现严重的bug最后我在MainCamera的相机中调用这个render函数MyPortal[] portals; void Awake() { portals FindObjectsOfTypeMyPortal(); } void OnPreCull() { for (int i 0; i portals.Length; i) { portals[i].Render(); } }解释一下这里的OnPreCull就是在裁剪之前我们得将所有的东西都渲染好,之后再执行裁切工作,虽然不放到这个函数也没什么太大的问题,但是还是要规范一下代码接下来看看效果确实有显示场景但是和我们预想的不一样,因为我们真正想让框框内出现的景象是框框内框框的景象,这里我们就要自己写一个shader来解决这个问题了下面我来解析一下shader的代码shader代码解析:Shader Custom/TiledTexture { Properties { _MainTex (Main Texture, 2D) white {} // 纹理属性 _Tiling (Tiling, Vector) (1, 1, 0, 0) // 平铺设置 } SubShader { Tags { RenderTypeOpaque } LOD 100 Cull Off Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc struct appdata { float4 vertex : POSITION; // 顶点位置 float2 uv : TEXCOORD0; // UV坐标 }; struct v2f { float2 uv : TEXCOORD0; // 传递UV坐标 float4 vertex : SV_POSITION; // 顶点位置 }; sampler2D _MainTex; // 纹理采样器 v2f vert (appdata v) { v2f o; o.vertex UnityObjectToClipPos(v.vertex); // 计算裁剪空间位置 o.uv v.uv; return o; } fixed4 frag (v2f i) : SV_Target { fixed4 texColor tex2D(_MainTex,i.uv); // 使用自定义UV采样纹理 return texColor; } ENDCG } } }值得一提的是,unity的shader和传统的opengl编写的shader是不一样的,可以说unity里面的shader要更加方便一些Properties:是在这个着色器上的一些属性_MainTex 表示在代码内部访问的变量名Main Texture表示在inspector界面中可以得到的看到的名字2D表示变量的类型white{}:前面的表示默认值,后面的表示设定参数设置这个cull off的原因是在进入传送门的时候会出现没有画面的情况,这是因为渲染器自动剔除了背面,而我们不需要提出背面接下来就是我们耳熟能详的正常一点的shader了这里之所以会出现刚才的那种情况是因为这个,我们将全部的纹理都放进去了o.uv v.uv;为了我们真正想要得到的东西,也就是框框里面的框框,我们需要进行裁剪,下面直接上代码Shader Custom/Portal { Properties { _MainTex (Main Texture, 2D) white {} // 添加这行来在Inspector中显示纹理设置 _InactiveColour (Inactive Colour, Color) (1, 1, 1, 1) } SubShader { Tags { RenderTypeOpaque } LOD 100 Cull Off Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc struct appdata { float4 vertex : POSITION; float2 uv :TEXCOORD; }; struct v2f { float4 vertex : SV_POSITION; float4 screenPos : TEXCOORD0; }; sampler2D _MainTex; float4 _InactiveColour; int displayMask; // set to 1 to display texture, otherwise will draw test colour v2f vert (appdata v) { v2f o; o.vertex UnityObjectToClipPos(v.vertex); o.screenPos ComputeScreenPos(o.vertex); return o; } fixed4 frag (v2f i) : SV_Target { float2 uv i.screenPos.xy/i.screenPos.w; fixed4 portalCol tex2D(_MainTex, uv); return portalCol * displayMask _InactiveColour * (1-displayMask); } ENDCG } } Fallback Standard // for shadows }下面我将讲解这其中的原理由于ComputeScreenPos(o.vertex);还没有抓话成为真正投影的坐标,而是一个四维的坐标,包含了透视信息,但是没有处理,这个时候我们就需要手动处理,也就是初上一个wreturn portalCol * displayMask _InactiveColour * (1-displayMask);至于这个是因为这样我就可以随意设置是否显示默认或者不显示下面就是我们正常显示的传送门了下面这个用来检测屏幕是否在相机的视野范围内,以后会讲到这个工具类是如何编写的// Skip rendering the view from this portal if player is not looking at the linked portal if (!CameraUtility.VisibleFromCamera (linkedPortal.screen, playerCam)) { return; }玩家的穿越效果首先我们想要做的就是根据玩家的和门的向量点积门的法向量判断正负从而判断是否要进行穿越为了让不仅仅是玩家穿越,我们要其他的物品可以穿越,为此我们专门定义了一个穿越类下面我将解释这个代码:using System.Collections.Generic; using UnityEngine; public class PortalTraveller : MonoBehaviour { public GameObject graphicsObject; public GameObject graphicsClone { get; set; } public Vector3 previousOffsetFromPortal { get; set; } public Material[] originalMaterials { get; set; } public Material[] cloneMaterials { get; set; } public virtual void Teleport (Transform fromPortal, Transform toPortal, Vector3 pos, Quaternion rot) { transform.position pos; transform.rotation rot; } // Called when first touches portal public virtual void EnterPortalThreshold () { if (graphicsClone null) { graphicsClone Instantiate (graphicsObject); graphicsClone.transform.parent graphicsObject.transform.parent; graphicsClone.transform.localScale graphicsObject.transform.localScale; originalMaterials GetMaterials (graphicsObject); cloneMaterials GetMaterials (graphicsClone); } else { graphicsClone.SetActive (true); } } // Called once no longer touching portal (excluding when teleporting) public virtual void ExitPortalThreshold () { graphicsClone.SetActive (false); // Disable slicing for (int i 0; i originalMaterials.Length; i) { originalMaterials[i].SetVector (sliceNormal, Vector3.zero); } } public void SetSliceOffsetDst (float dst, bool clone) { for (int i 0; i originalMaterials.Length; i) { if (clone) { cloneMaterials[i].SetFloat (sliceOffsetDst, dst); } else { originalMaterials[i].SetFloat (sliceOffsetDst, dst); } } } Material[] GetMaterials (GameObject g) { var renderers g.GetComponentsInChildrenMeshRenderer (); var matList new ListMaterial (); foreach (var renderer in renderers) { foreach (var mat in renderer.materials) { matList.Add (mat); } } return matList.ToArray (); } }public GameObject graphicsObject;表示需要传送的游戏对象的图形表示模型或外观。public GameObject graphicsClone { get; set; }保存对象的克隆用于在传送门另一侧显示的镜像。public Vector3 previousOffsetFromPortal { get; set; }保存对象与传送门之前的相对位置用于在传送门中跟踪对象的位置变化。public Material[] originalMaterials { get; set; }保存原始图形对象的材质列表。public Material[] cloneMaterials { get; set; }保存克隆对象的材质列表。public virtual void Teleport这个方法处理对象从fromPortal传送到toPortal的行为。传送后的对象位置和旋转角度被设置为pos和rot。该方法是virtual的允许在派生类中重写以提供不同的传送行为。public virtual void EnterPortalThreshold ()当对象进入传送门的边界时调用此方法。其功能如下检查是否已经创建了graphicsClone图形克隆对象。如果没有创建则通过Instantiate创建一个克隆对象设置它的父对象和缩放比例并获取graphicsObject和graphicsClone的材质。如果克隆对象已经存在则直接激活它。这样可以在传送门中创建对象的镜像效果。public virtual void ExitPortalThreshold ()当对象离开传送门时非传送的情况下调用该方法隐藏graphicsClone表示对象已经离开传送门。将原始材质的sliceNormal向量设置为Vector3.zero用于禁用切片效果传送门通常通过切片效果创建镜像。public void SetSliceOffsetDst (float dst, bool clone)这个方法设置切片效果的偏移距离用于控制对象在传送门内外的可见性例如部分被遮挡的效果遍历所有的材质根据clone参数决定是操作cloneMaterials还是originalMaterials。使用SetFloat方法来更新材质的sliceOffsetDst属性控制材质切片效果的偏移。Material[] GetMaterials (GameObject g)此方法用于获取指定游戏对象的所有材质列表获取游戏对象的所有MeshRenderer组件。遍历每个MeshRenderer的材质并将其添加到列表中。返回一个包含所有材质的数组用于originalMaterials和cloneMaterials属性的初始化。相机工具类public static bool VisibleFromCamera (Renderer renderer, Camera camera) { Plane[] frustumPlanes GeometryUtility.CalculateFrustumPlanes (camera); return GeometryUtility.TestPlanesAABB (frustumPlanes, renderer.bounds); }第一个函数是用来得到相机视锥的六个平面的第二个函数是用来判断render是否在这些平面内或者相交