Well, I loved those games when I was little. I really loved playing with them and the Crash series. That time I don’t really cared about the technical side of the games, I just played them, but now, as an aspiring game developer, I mainly focus on the programming side, and how special effects can be achieved.

Days ago, I found my old Spyro CD’s, downloaded ePSXe, and start playing. That’s the time I got some inspiration for my low-poly exploration game. The portals. I wanted to exactly reprocude those gates and use them as a main feature. Here is a gif what it looks like ingame

Okay, but why did I start to wrote this write-up? Some people on reddit wanted to know about this technique, and as I found nothing on this topic, and successfully implemented this in Unity, I sat down, and started to write this piece of text.

I try to illustrate the article with pictures for better understanding.

Firstly, what you have to do is create a mesh for your portal. It can be any mesh like a pentagon like in Spyro games, or like a arc of stone like mine:

EDIT: There is a much simpler method: Create an inner polygon which represents the portal, and apply the PortalShader to it! LINK

The next step is to make a plane inside of it, and try to resize it so it is as big as the gate like this (make sure you create this plane as a separate object!!!) :

Okay, so you set up the plane where you will render the portal’s background. The next step is to create a mask that will be used to clip the plane mesh so only the inner part will be shown. I’ll show the wireframe of mine, so you can get the idea (this have to be a separate object too!!!) :



I have to insert a small rectangles on the side, because if you viewed the gate from the side, a small part of the texture plane would be shown. It can be skipped out, if the mask and the plane is at the same position, but I haven’t tried out, so maybe I won’t work, but try out if you want :)

Okay, so we created our mesh to use in Unity. Import it to Unity and place it down.

Create a new script called SetRenderQueue. I use C#, but you can easily translate it to JavaScript. Here is the code:

using UnityEngine; [AddComponentMenu("Rendering/SetRenderQueue")] public class SetRenderQueue : MonoBehaviour { [SerializeField] protected int[] m_queues = new int[]{3000}; protected void Awake() { Material[] materials = renderer.materials; for (int i = 0; i < materials.Length && i < m_queues.Length; ++i) { materials[i].renderQueue = m_queues[i]; } } }

Drop this script on the mask and the plane object, and on the mask set the queue to 3000, on the plane, set it to 3010. Next, add a texture to the plane:



Create a new shader and call it DepthMask. Overwrite the default shader with this code:

Shader "Masked/Mask" { SubShader { // Render the mask after regular geometry, but before masked geometry and // transparent things. Tags {"Queue" = "Geometry+10" } Pass { // Don't draw in the RGBA channels; just the depth buffer ColorMask 0 ZWrite On Stencil { Ref 1 Comp always Pass replace } } } }

What this shader does is, it writes the depth values of the mask mesh into the depth buffer, so everything drawn before it (not as positions, but as in render queue as this will be drawn in the queue of 3010 as Unity’s Geometry queue is 3000. You can read more of RenderQueue’s here) will be discarded.

What is that stencil pass? (Read more on Unity’s Stencil buffer here)

Stencil buffer is a buffer with size of ScreenWidth * ScreenHeights. This buffer can hold a value (usually a 8-bit value, so a max of 255) for every pixel on the screen. This shader writes a 1 (as stated by Ref) to every pixel our mask covers. You can use a lot of functions in Comp, but we’ll see this later. In this shader we use the Comp always. Comp stands for Compare and as we set it to always the stencil check will always pass. The pass keyword states what should we do when the stencil test passes. We set it to replace, so it will replace the current stencil value at that pixel to our Ref value, in this case 1. We will use this value in an other shader to clip the sticking out parts of our texture plane so this part of the shader is very important!

Okay, so add this shader to our mask plane. You will see that in the editor the plane will become invisible. It’s okay as we didn’t write in the color buffer.



For the next step create a new shader called PortalShader.

Here is the source code:

Shader "Custom/PortalShader" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 200 Stencil { Ref 0 Comp Equal } CGPROGRAM #pragma surface surf Lambert sampler2D _MainTex; struct Input { float2 uv_MainTex; float4 screenPos; }; void surf (Input IN, inout SurfaceOutput o) { float2 screenUV = IN.screenPos.xy / IN.screenPos.w; half4 c = tex2D (_MainTex, screenUV); o.Albedo = c.rgb; o.Alpha = c.a; } ENDCG } }

Explanation incoming:

First we create a property for our texture, called _MainTex. Next in the stencil pass we check with the Equal compare operator if the current stencil value at that pixel is 0, the default value. Let’s check our DepthMask shader. We set the stencil value to 1 if we rendered that pixel ( in reality we only wrote to the depth buffer ). So the stencil test will only pass if we did not set the stencil value, so we did not rendered our mask. (There is an additional depth test with the stencil test which can be used for other usage, but we don’t need that here)

Our input struct differs from the default input structure as we use an other field with the tyep of float4. With screenPos input, we can acquire the position of the current pixel on the screen.

In the surf function, we get the normalized coordinates of the screen by taking the X and Y coordinates of the screenPos variable, and divide it by it’s W value. It’s not the usual normalized but now X and Y is between [0.0, 1.0]. We use this coordinates to sample our texture and set the output’s albedo to that color.

Now use this shader on our plane mesh, and drop a texture on it. Now even in the editor, if you move the camera around the portal you can see that it works like the gates in Spyro!

See it in motion

I hope you liked this article! If you have any question feel free to comment on reddit, write a PM to me on reddit, or you can comment here. Follow this blog for more articles and stories about game dev :)