A dragon that creates portals
Made by Jay Fairouz
Table of content
1. Introduction

Teleportation is in my opinion one of the coolest super powers. Dragons are usually portrayed as beings with magical powers, so I thought it was a good idea to combine the two.
I made a dragon that can shoot an orb that spawns two portals. The dragon is able to fly through these portals. The portals look correct from multiple angles and the position can be modified during run time.
My intention for this system is possible implementation in an actual game and getting more proficient with creating new game mechanics.
If you would like to learn more about how portals work I would encourage you to read through Ilett’s (2019) forum posts and inspect Lague’s (n.d.) project.
2. Making portals
2.1 The gameobjects

The first important step to making portals is to make a GameMaster gameobject and a Portal, PortalCamera and PortalSpawner (Portal Orb in the image) prefab. In the GameMaster we’ll keep track of the portals and their information.
We use the portalSpawner to instantiate new portals and their cameras and we send their information to the GameMaster.
void OnDeath() { //make the cameras for (int i = 0; i < portals.Length; i++) { cameras[i] = Instantiate(cameraObject, new Vector3(transform.position.x + 10 * i, transform.position.y, transform.position.z), Quaternion.identity); GameMaster.GetComponent<PortalSetUp>().MakeNewRenderTexture(cameras[i]); cameras[i].transform.parent = portalCameras.transform; } //portal rotation values var x = UnityEditor.TransformUtils.GetInspectorRotation(gameObject.transform).x; var y = UnityEditor.TransformUtils.GetInspectorRotation(gameObject.transform).y; var z = UnityEditor.TransformUtils.GetInspectorRotation(gameObject.transform).z; Quaternion rotation = Quaternion.Euler(x, y, z); //make the portals for (int i = 0; i < portals.Length; i++) { if(i == 0) //the first portal portals[i] = Instantiate(portalObject, transform.position, rotation); else // the second portal, rotated 180 degrees on the y and put behind the other portal portals[i] = Instantiate(portalObject, transform.position + transform.forward * -75, Quaternion.Euler(-x, y + 180f, -z)); portals[i].transform.parent = portalCollection.transform; GameMaster.GetComponent<PortalSetUp>().AssignMaterialToPortal(portals[i], i); } // link cameras to the portals GameMaster.GetComponent<PortalSetUp>().AssignPortalsToCamera(); //destroys this object, and it's clone GetComponent<PortalableObject>().DestroyObject(); }
I also gave the portalSpawner a velocity so that it would move faster forward than the dragon, and create the portal ahead of it.
void Move() { Rigidbody rb = GetComponent<Rigidbody>(); Vector3 forward = transform.TransformDirection(Vector3.forward) * 10; rb.velocity = forward * speed * Time.deltaTime; }
2.2 GameMaster

The GameMaster object uses the PortalSetUp script. In that script we will keep track of how many portals there are and their cameras, materials and render textures.
Because the dragon will be able to create multiple pairs of portals at once, we need to create our render textures and materials through code. It’s important that each portal has its own render texture and material, because that’s what makes it look like a portal to another position.
In the MakeNewRenderTexture() function we put the camera gameobject that we got from the portal script in the “cameras” list. Furthermore, we make a new render texture and a new material and assign each to a list as well.
public void MakeNewRenderTexture(GameObject cameraGameObject) { cameraGameObject.name = "Camera " + cameras.Count; cameras.Add(cameraGameObject); Camera camera = cameraGameObject.GetComponent<Camera>(); if (camera.targetTexture != null) { camera.targetTexture.Release(); } camera.targetTexture = new RenderTexture(Screen.width, Screen.height, 24); renderTextures.Add(camera.targetTexture); //create a new material Material material = new Material(Shader.Find("Unlit/ScreenCutoutShader")); material.name = "NewPortalMat" + materials.Count; material.mainTexture = camera.targetTexture; materials.Add(material); }
In the MakeNewRenderTexture we use the “Unlit/ScreenCutoutShader” shader for our material. This is used to only display the appropriate view on the material, decided by the screen position. This shader was written by Brackeys (n.d).
sampler2D _MainTex; fixed4 frag (v2f i) : SV_Target { i.screenPos /= i.screenPos.w; fixed4 col = tex2D(_MainTex, float2(i.screenPos.x, i.screenPos.y)); return col; } ENDCG
The next thing we do is assign the materials to the portals, we use the AssignMaterialToPortal() function in PortalSetUp for that.
public void AssignMaterialToPortal(GameObject portal, int i) { //Child(0) is the mesh portal.transform.GetChild(0).gameObject.GetComponent<Renderer>().material = materials[portals.Count]; portal.name = "Portal " + portals.Count; portals.Add(portal); }
The last thing that we’ll do in the PortalSetUp script is assign the “otherPortal” and the correct camera to a portal. The portals come in pairs, so we have to assign each portal in a pair to each other. We do that by looping through each portal and if it’s an even portal we assign the next portal as the otherPortal, and if it’s uneven we assign the previous portal.
We also do something similair for the camera, we store the next/previous camera, because each portal uses it’s partners camera for it’s texture.
public void AssignPortalsToCamera() { for(int i = 0; i < portals.Count; i++) { if (i % 2 == 0) // if i is even { portals[i].GetComponent<Portal>().otherPortal = portals[i + 1]; portals[i].GetComponent<Portal>().portalCamera = cameras[i + 1]; } else { portals[i].GetComponent<Portal>().otherPortal = portals[i - 1]; portals[i].GetComponent<Portal>().portalCamera = cameras[i - 1]; } } mainCamera.GetComponent<PortalCamera>().UpdateLists(portals, cameras, renderTextures); }
2.3 Resizing the portal
I thought that it looked quite boring to shoot an object and then suddenly create the portals. So I decided to make the portals smaller upon creation and make them bigger over time.
The portal gameobject has multiple children, among those there is a gameobject that contains the mesh and a gameobject that has the particle effect. The mesh size is quite easy to manipulate, I could just change the scale of the whole portal object and it would apply it to the mesh as well. Particle effects on the other hand worked quite differently.
As you can see in the following image the particle effect is unaffected by the parent changing in size:

That’s why I need to adjust the particle system size in a different way, I used the particle effect shape radius for this.
I changed the size of the whole gameobject in a coroutine, so it could be done in an extended amount of time without interupting other systems.
IEnumerator ScaleOverTime(float time) { ParticleSystem.ShapeModule ps = particleChild.shape; float originalRadius = ps.radius; Vector3 originalScale = transform.localScale; float currentTime = 0.0f; do { ps.radius = Mathf.Lerp(originalRadius, destinationRadius, currentTime / time); transform.localScale = Vector3.Lerp(originalScale, destinationScale, currentTime / time); currentTime += Time.deltaTime; yield return null; } while (currentTime < time); // sets the exact value, because Lerp never get's there if(currentTime >= time) { ps.radius = destinationRadius; transform.localScale = destinationScale; yield return null; } }
3. Camera
We’ll now focus on the Main Camera gameobject. The Main Camera uses the PortalCamera script. In the Portal Camera script we apply the portal textures before we render the camera, we do this using the OnPreRender() function. We want to do this, because the portal visuals might become obscured if the main camera renders before it.
private void OnPreRender() { int cameraAmount = cameras.Count; if (cameraAmount > 0) { for (int i = 0; i < cameraAmount; i++) { if (portals[i].GetComponent<Portal>().IsRendererVisible()) { portalCamera = cameras[i].GetComponent<Camera>(); portalCamera.targetTexture = renderTextures[i]; for (int j = iterations - 1; j >= 0; --j) // render the recursion { if (i % 2 == 0) // if i is even RenderCamera(portals[i].GetComponent<Portal>(), portals[i + 1].GetComponent<Portal>(), j, i); else RenderCamera(portals[i].GetComponent<Portal>(), portals[i - 1].GetComponent<Portal>(), j, i); } } } } }
In the RenderCamera() function we set the portal camera position and rotation correct in relation to the Main Camera. We do this in a for loop to account for the camera position and rotation when there is recursion.
I started my project in URP and wasn’t able to get the recursion working there because it didn’t support OnPreRender(). There are other solutions for this problem, but since I also needed some specific shaders to work that also didn’t work in URP it was around this point of the project that I decided to go back to the old system.
private void RenderCamera(Portal inPortal, Portal outPortal, int iterationID, int currentCamera) { Transform inTransform = inPortal.transform; Transform outTransform = outPortal.transform; Transform cameraTransform = portalCamera.transform; cameraTransform.position = transform.position; cameraTransform.rotation = transform.rotation; for (int i = 0; i <= iterationID; ++i) { // Position the camera behind the other portal. Vector3 relativePos = inTransform.InverseTransformPoint(cameraTransform.position); relativePos = Quaternion.Euler(0.0f, 180.0f, 0.0f) * relativePos; cameraTransform.position = outTransform.TransformPoint(relativePos); // Rotate the camera to look through the other portal. Quaternion relativeRot = Quaternion.Inverse(inTransform.rotation) * cameraTransform.rotation; relativeRot = Quaternion.Euler(0.0f, 180.0f, 0.0f) * relativeRot; cameraTransform.rotation = outTransform.rotation * relativeRot; //adjusting the near clipping plane cameras[currentCamera].GetComponent<Camera>().nearClipPlane = Vector3.Distance(cameras[currentCamera].transform.position, outTransform.position); } portalCamera.Render(); }
At the end of the function we adjust the camera nearClipPlane so we don’t get unwanted objects between the camera and the portal. Here’s an example of what it looks like if we don’t adjust it.
4. Portal teleportation
4.1 Make a clone
Now that our portals look nice visually, we can add the teleportation functionality to it. We need two scripts for this. A portalableObject script for each object that can travel through portals, and a portal script.
In the portalableObject script we start of with making a clone of the gameobject. This clone will appear in the other portal to make it seem like the original gameobject is at two places at once. The clone doesn’t need additional scripts, a hitbox, an audio listener, etc. So we have to remove them. You could also make a clone by making an empty and filling this with objects that you need, but that was more complicated for me, because my dragon object has 60 children gameobjects. We will start the clone inactive, we only want to see it when the original gameobject is inside a portal.
private void Start() { cloneObjectContainer = GameObject.Find("Portalable Object Clones"); FindChildren(gameObject); // if it's a clone object if (gameObject.name.Contains(" clone")) { // assigns the clone gameobject to the main object, and removes certain components from the clone ManageCloneGameObject(); } else { // enable the collider, disable all child PortableObject scripts, check if it has a camera ManageOriginalGameObject(); } }
In the ManageCloneGameObject() and ManageOriginalGameObject() functions we used a list containing the children of the current gameobject. We fill that list in the FindChildren() function (used in the Start). This is a recursive function that will call itself until it reaches a gameobject without children.
void FindChildren(GameObject parent) { for (int i = 0; i < parent.transform.childCount; i++) { GameObject child = parent.transform.GetChild(i).gameObject; childCount++; allChildren.Add(child); FindChildren(child); } }
4.2 Move the objects
The clone movement will be handled in the LateUpdate. First we check if the object is colliding with a portal, after that we apply the movement if the clone object is active. If the clone isn’t active its position will be set to a location really far away. Most of the portal movement is based on the work of Ilet (2019).
private void LateUpdate() { if (inPortal == null || outPortal == null) { return; } if(cloneObject.activeSelf) { var inTransform = inPortal.transform; var outTransform = outPortal.transform; // Update position of clone. Vector3 relativePos = inTransform.InverseTransformPoint(transform.position); relativePos = halfTurn * relativePos; cloneObject.transform.position = outTransform.TransformPoint(relativePos); // Update rotation of clone. Quaternion relativeRot = Quaternion.Inverse(inTransform.rotation) * transform.rotation; relativeRot = halfTurn * relativeRot; cloneObject.transform.rotation = outTransform.rotation * relativeRot; } else { cloneObject.transform.position = new Vector3(-1000.0f, 1000.0f, -1000.0f); } }
When the portalableObject is in a portal we call the SetIsInPortal() function in portalableObject from the Portal script. That function will set the clone active and switch the active camera to that of the clone. I chose to do it this way instead of only using one camera, because that caused issues when the original object moved backward through a portal.
public void SetIsInPortal(Portal inPortal, Portal outPortal, Collider wallCollider) { fullPortalMovement = !fullPortalMovement; //otherwise the camera stutters when transitioning this.inPortal = inPortal; this.outPortal = outPortal; Physics.IgnoreCollision(collider, wallCollider); cloneObject.SetActive(true); if (fullPortalMovement && hasCamera) { cloneCameraObject.SetActive(true); ownCameraObject.SetActive(false); } ++inPortalCount; }
The Warp() function is the actual teleporting part of the script. Here we change the position, rotation and velocity of the original object. We also set the original camera active again. Warp() gets called in the portal script whenever the colliding gameobject with a PortalableObject component moves out of the portal.
public virtual void Warp() { if (hasCamera) { ownCameraObject.SetActive(true); cloneCameraObject.SetActive(false); } var inTransform = inPortal.transform; var outTransform = outPortal.transform; // Update position of object. Vector3 relativePos = inTransform.InverseTransformPoint(transform.position); relativePos = halfTurn * relativePos; transform.position = outTransform.TransformPoint(relativePos); // Update rotation of object. Quaternion relativeRot = Quaternion.Inverse(inTransform.rotation) * transform.rotation; relativeRot = halfTurn * relativeRot; transform.rotation = outTransform.rotation * relativeRot; // Update velocity of rigidbody. Vector3 relativeVel = inTransform.InverseTransformDirection(rigidbody.velocity); relativeVel = halfTurn * relativeVel; rigidbody.velocity = outTransform.TransformDirection(relativeVel); // Swap portal references. var tmp = inPortal; inPortal = outPortal; outPortal = tmp; }
5. Conclusion
I’m quite happy with how the portals turned out. Sadly, I wasn’t able to partially hide a gameobject when it’s traveling through a portal before the deadline was due. I plan to still implement this and make a small game around the portal mechanic.
Other things that I could improve:
- There is some minor camera clipping.
- Animations can desync in portals.

6. Bonus: Dragon and environment
At first, I applied the sin wave movements to the vertices of the model, but that wasn’t the best idea. It caused the model to get deformed and it didn’t allow me to keep track of the actual positions of the model.
Considering that the model consisted of multiple parts, I thought of putting the parts in an array and animating those. That worked out nicely.
void AnimateParts() { for (int i = 0; i < partTransforms.Length; i++) { Vector3 pos = partTransforms[i].position; float offsetZ = waveHeightZ * Mathf.Sin(Time.time * movementSpeedZ + GetHeadDistance(pos)); float offsetY = waveHeightY * Mathf.Sin(Time.time * movementSpeedY + GetHeadDistance(pos)); partTransforms[i].localPosition = new Vector3(nonAnimatedPositions[i].x, offsetY + nonAnimatedPositions[i].y, -offsetZ + nonAnimatedPositions[i].z); } }
The last step was to add an environment for the dragon. I added a desert (Unity, n.d.), vegetation (Lague, n.d.), rocks (Lague, n.d.), a sky shader (Lague, n.d.) and a pokemon center.
Sources
- Brackeys. (n.d.). Brackeys/Portal-In-Unity. GitHub. Accessed 29 March 2021, at https://github.com/Brackeys/Portal-In-Unity
- Ilett, D. (2019, 1 December). Portals | Series Introduction. Daniel Ilett: Games | Shaders | Tutorials. https://danielilett.com/2019-12-01-tut4-intro-portals/
- Lague, S. (n.d.). SebLague/Portals. GitHub. Accessed 29 March 2021, at https://github.com/SebLague/Portals/tree/master
- Unity. (n.d.). Unity-Technologies/VisualEffectGraph-Samples. GitHub. Accessed 29 March 2021, at https://github.com/Unity-Technologies/VisualEffectGraph-Samples