A Look at Spyro’s Portals – Rendering the Background

Fracture Hills portal as seen in Spyro Reignited Trilogy (Toys for Bob, 2019)

While I was at university I discovered shaders, and I instantly became very interested in writing my own shader to the point that I used my dissertation to learn the basics of shader writing. Since graduating I started experimenting more with shaders to create some interesting effects. Today I’m going to share with you my take on Spyro Reignited Trilogy’s portal shaders.

Before we proceed, keep in mind that I’ve only recently graduated from university, and my shaders experience is fairly small and I’m still learning new things each day! With that said, you might see some crazy things, but I’m always happy to learn so feel free to comment down below any suggestions. Now, on to the shader.

spyro-themed-portal-shader

Original Shader Breakdown

I’m a big fan of Spyro games, I remember instantly falling in love with the games when I played them for the first time; it was a mix of the gameplay and the art style that made me fall in love with the series. After playing Reignited Trilogy I was happy to learn that the games felt just like the original games but with a modern graphical twist.

One thing that I always liked about was the portals and the little animation between the levels where the gems would fly from the top of the screen to the bottom, unfortunately that is gone from the Reignited Trilogy, but the portals are outstanding!

Naturally, of course the first game that I use as inspiration and learning is Spyro. Portals in Reignited Trilogy look outstanding and very complex, but that is because there are some fancy effects used, and recreating them wasn’t as hard as I imagined! So let’s see:

Example portal from Spyro: Reignited Trilogy; the background looks like it’s another world we’re looking into with a nice distortion and outline effects.

There are few things happening in this shader, and I will cover each thing in a separate blog post:

  • Background (using stencil shader)
  • Distortion
  • Intersection (outline as seen on the sides)
  • Extra Effects (smoke particles, lights, etc.)

If you look closely at the gif, you will notice that as you move the camera around you can see more “inside” of the other world, this isn’t something you can achieve using textures (which I’ve learnt in my second approach) and in the end I used stencil buffer shader, but let’s start from the beginning and my first approach.


Creating the Portal Effect

In this post I will describe in depth my final approach to re-creating this background effect but I will also briefly mention my previous approaches and why they have failed. Link to a Github repo can be found at the end, where you can find all the code for the final version as well as my previous attempts.

Using Cubemap Textures

My first approach to recreating this background was using cubemap textures. If you’re not familiar with cubemap textures they’re essentially textures made up of 6 squares that are mapped onto an imaginary cube, that cube is then displayed to the user; in our case it was inside the portal and looked like a skybox.

When first researching this, I’ve came across a blog by Daniel Illett in which he describes how to achieve a very simple Spyro portal using cubemap textures (you can find it here); I decided to follow Daniel’s post and see what I can do with it.

Portal effect using cubemap texture; in this version as you move the camera closer/further from the portal the skybox moves with our view (gets bigger/smaller), and this isn’t what we want.

It’s a little easier to understand when you can actually see it in action, from the gif above you can see that when we move closer to the portal we can see more of the “world”, that is because the cubemap texture is mapped onto the imaginary cube and essentially acts like a skybox that is placed inside our portal.

The first issue you can probably see, as we move on the “z” axis (or “y” axis for you Unreal folk) the world moves with us, that is because what is actually displayed inside the portal is based on our view direction, and as we move around so does the texture inside the portal. This is how the portals looked in the original games, but with the new games they changed the behaviour.

In our case we want the background to stay in place, and this is probably possible with some clever maths, but in my research I couldn’t find anything about this so I abandoned ship.


Second Camera & Render Texture

In my second approach I’ve found out something interesting about Spyro Reignited Trilogy, if you glitch your way out of bounds and fly outside of the skybox you will notice that all the skyboxes for levels accessible from your current hub are also loaded in; this got me thinking, maybe I could use a second camera and render texture to display the effect?

Skyboxes for other levels which can be seen outside of the currently loaded level.

If you’re not familiar with render textures you can read up on them here on the Unity website. Essentially cameras in Unity have an option to render their current output to a texture, instead of the monitor, which you can use in materials just like a normal 2D texture.

For this approach I created a sphere away from the portal, this was going to be the skybox for our portal, and I placed a camera inside of that sphere; with help of a custom shader I was able to reverse the normals of that sphere, so the camera inside can see the skybox texture, and I used a custom NoLighting function to make sure the skybox is as bright as the texture.

This is what the skybox looks like with reversed normals shader.

In this shader I also use custom culling, because I want the front faces to be “hidden” otherwise they might clip into the camera. If you’re not familiar with culling, it is a technique used to optimize video games and make it easier for our computers to play video games (culling is used not just in video games).

In normal culling settings, the technique is applied to the back of the objects because we can’t see those faces we don’t need to render them, this applies to the inside of the objects, etc. In case we do need to render the inside of our sphere, we need to apply a custom culling to our shader.

In order to flip the normals on the sphere I multiply the xyz direction of the normals by “-1” which returns the opposite direction (flipped) of the normals and I use that as the normal direction. If this sounds confusing to you, feel free to look at the ReverseNormals.shader file on the project’s Github repo. This has to be done in the vertex function of the shader.

Vertex function is used to modify the vertices, faces and points of the model; this is not possible inside of the surface function, because the surface function is used to manipulate the pixels of the object. You can read more about vertex, fragment and surface shaders here on Unity’s docs.

Portal effect using a second camera and a render texture; this approach looks “flat” as it lacks the depth, while the original keeps the depth.

When I put this together and I noticed one very big issue about this approach, because we are rendering our view onto a flat 2D texture, we’re instantly losing all of the depth and half of the effect of “looking into another world”; for this reason I scrapped this approach.

In the gif above you can see that the effect is very “flat”, especially when we look to the sides; I have added a script to my skybox camera which makes it rotate with the player camera but it still looked very flat and dull. I have once again begun researching again.


Stencil Shader

Enter the magic of stencil shaders. I have found out about stencil shaders only very recently and I don’t have any experience in using them, so for this reason I went out to see if anyone has made some tutorials for stencil shaders.

I found out a tutorial from Alan Zucconi in which he talks about Non-Euclidean Cubes, this is an effect seen a lot in a game called Antichamber which aims to mess with our brains by creating the “impossible geometries”. In his example, Alan uses stencil shaders to create this very effect (you can see the final version on my Twitter here).

Magic of non-euclidean cubes, each face of the cube shows something different inside.

While watching a video by Glader, in the video you will notice that Glader places the skybox on top of the portal but the sphere is invisible to the main camera, and a stencil shader is used to recreate the original Spyro portals. I decided that this approach could work for my shader, so I got to work.


Creating the Shader

Properties
{
    _StencilMask("Stencil Mask ID", int) = 0
    _MainTex("Skybox Texture"), 2D) = "white" {}
}
SubShader
{
    Tags { "RenderType"="Opaque" "Queue"="Geometry-2" }
    Cull Front
    LOD 200
     
    Stencil
    {
        Ref[_StencilMask]
        Comp equal
    }
 
    CGPROGRAM
    #pragma surface surf NoLighting vertex:vert
    #pragma target 3.0
 
    sampler2D _MainTex;
 
    struct Input
    {
        float2 uv_MainTex;
    }
 
    // Custom lighting function
    fixed4 LightingNoLighting(SurfaceOutput s, fixed3 lightDir, fixed atten)
    {
        fixed4 c;
        c.rgb = s.Albedo;
        c.a = s.Alpha;
        return c;
    }
 
    void vert(inout appdata_full v)
    {
        // Reversing the normal direction
        v.normal.xyz = v.normal * -1;
    }
 
    void surf (Input IN, inout SurfaceOutput o)
    {
        fixed4 c = tex2D(_MainTex, IN.uv_MainTex);
        o.Albedo = c.rgb * 0.0;
    }
    ENDCG
]

First, a quick breakdown of the reverse normals shader; starting from the top this shader only needs one texture, the skybox texture; you’ll noticed that I’m using custom cull (culling) setting, this is because, as mentioned before, I want to be able to see the inside of the sphere and I don’t want the front faces to block the camera’s view; we hide the faces closer to the camera.

As mentioned before, I’m using a custom lighting function to make sure the skybox isn’t affected by any lights in the scene.

You will notice the _StencilMask property, this property tells our stencil buffer on which “layer” this object is and whether it should be rendered or not; if it doesn’t make much sense right now, it will later.

Inside the stencil block we can find two lines of code; the first “ref” is the reference for other shaders, this is the value that will be used to compare with other shader’s stencil blocks, in our case we want the _StencilMask property to be used as the value. Next is the comparison function, this determines the behaviour of the stencil buffer and in our case “equal” means that if the reference values are equal the shape will be rendered (more info in Unity’s manual).

Next we need to actually display the stencil cutout and for this we will create another surface shader and call it “stencil cutout”; in this shader we will display the result of the stencil buffer comparison (the skybox).

Properties
{
    _StencilMask("Stencil Mask ID", int) = 0
}
SubShader
{
    Tags { "RenderType"="Opaque" "Queue"="Geometry-2" }
    ColorMask 0
    ZWrite Off
     
    Stencil
    {
        Ref[_StencilMask]
        Comp always
        Pass replace
    }
 
    CGPROGRAM
    #pragma surface surf Lambert noshadow
    #pragma target 3.0
 
    struct Input
    {
        // We need a property inside Input, otherwise an error will be thrown
        float4 vertex;
    }
 
    void surf (Input IN, inout SurfaceOutput o)
    {
        o.Albedo = fixed4 c = (0, 1, 0, 1);
    }
    ENDCG
]

The code here is fairly similar, but there are few key differences; first is the “ColorMask 0” tag, this tells Unity that we don’t want this shader to write to the color channels. Next is the “ZWrite Off” tag, this tells Unity that this shader won’t be writing into the depth buffer, meaning that it will act as if the object isn’t there because it isn’t written in the depth buffer and won’t be blocking our skybox.

So with those two shaders, let’s put together our portal. We want to create our skybox first, and apply the reverse normals shader to it, I will create a material called MAT_SKYBOX and use the ReverseNormals shader and apply it to the sphere. You should see this:

Skybox with the reverse normals shader applied.

Now we want to set the stencil mask on our skybox material to “1” and you will see that he sphere has disappeared, this means its working correctly, now we want to add a quad inside of our portal and create another material for it which I called MAT_StencilCutout; when we set the stencil mask of our stencil cutout material to “1” you will see that the skybox is now visible inside of the portal.

We can now see the skybox through the portal, but the ground is also visible as well as the portal behind the portal quad.

Perfect! We’re making progress with the background, but if you noticed, the ground is also displayed through the portal and that is not something we want! In order to get rid of this, we will create a new shader and we will call it “Blocker” and we’ll create a new quad and a new material for it; I called the material “MAT_BLOCKER” and I placed the quad in front of the stencil cutout quad.

Properties
{
    _StencilMask("Stencil Mask ID", int) = 0
}
SubShader
{
    Tags { "RenderType"="Opaque" "Queue"="Geometry-2" }
    ColorMask 0
    ZWrite Off
     
    Stencil
    {
        Ref[_StencilMask]
        Comp always
        Pass replace
    }
 
    CGPROGRAM
    #pragma surface surf Lambert noshadow
    #pragma target 3.0
 
    struct Input
    {
        // We need a property inside Input, otherwise an error will be thrown
        float4 vertex;
    }
 
    void surf (Input IN, inout SurfaceOutput o)
    {
        o.Albedo = float4(0, 1, 0, 1);
    }
    ENDCG
]

This code looks very similar to the previous shaders, with few key differences. First is the “GrabPass” function, if you’re not familiar with it you can read more in Unity’s manual, but in short, grab pass is a type of shader pass which captures the content of the screen where the object is about to be drawn and stores it in a texture (which we will use later).

Once again we use color mask because we don’t want to write to the RGB channels and we have the stencil block, but this time in the comparison function we say that we want to keep the stencil buffer when the comparison function returns equal. We don’t have to worry about Input because we don’t actually use it but we need a property in there otherwise the shader will return errors and in the surf function we set the albedo to green but we won’t see that because we’re not writing to RGB channels.

So now if we apply the shader to our new blocker material and we place the quad in front of the stencil cutout we should see something like this:

If you look closely, the portal no longer shows the ground or the portal model behind the quad.

As you can see, we can no longer see the ground and model of the portal “on the other side”! This is exactly what we wanted, so now we’re off to the final step, let’s display this on an additional plane so that in the next part we can add some effects to this shader!

In order to do this we will need to create a new quad, this is the final one I promise, and we will create a new material called “MAT_PortalInner” (feel free to name them what you want, I just keep the same names as in the repo so it’s easier to compare), and we’ll place this quad in front of the blocker.

Properties
{
 
}
SubShader
{
    Tags { "RenderType"="Opaque" }
    LOD 200
 
    CGPROGRAM
    #pragma surface surf Standard fullforwardshadows vertex:vert
    #pragma target 3.0
 
    struct Input
    {
        float4 vertex;
        float4 screenPos; // Built in Unity variable
    }
 
    sampler2D _GrabTexture;
 
    void vert (inout appdata_full v, out Input o)
    {
        UNITY_INITIALIZE_OUTPUT(Input, o);
        o.vertex = UnityObjectToClipPos(v.vertex);
    }
 
    void surf (Input IN, inout SurfaceOutput o)
    {
        float4 grabPassTexture = tex2Dproj(_GrabTexture, IN.screenPos);
        o.Albedo = grabPassTexture;
    }
    ENDCG
]

This code is the base which I will use for the future posts when we create the full portal effect, I will refer back to this shader but I will be adding new code to it, so make sure you have it at hand if you want to follow the next posts.

So you might wonder, that is happening here? Well not much at the moment; first we want to get our screen position into our shader, thankfully we can do this easily in Unity by creating a screenPos variable inside the Input struct and Unity will automatically pass the value to our shader.

Next we sample the _GrabTexture, this is just a normal 2D sampler that you’d use for any other texture. Inside of the vertex function we don’t do much at the moment, but we will use it later on; at the moment we just initialize the output and we transform our point from object space to camera’s clip space using Unity’s built-in macro and store it in the vertex variable.

Next we can use our grab texture inside of the surface function, in order to do this we need to sample it using tex2Dproj function, Nvidia’s explanation of tex2Dproj, because unlike tex2D, tex2Dproj will divide input’s xy values by the w before sampling. Finally we can set the albedo to our new grabPassTexture and we should see the same result as with blocker!

This is what the portal should look like in the inspector:

So now we have all of the fundamentals done for our shader, and we can proceed with creating the distortion effect and the outline glow effects! But this will come in the next posts because I feel like this blog post is long enough already, and I wanted to show you my previous approaches and how I have tackled the issues I’ve come across.

Thank you very much for reading, and I hope you have a great day!


GitHub Repo: https://github.com/danielpokladek/personal-shaders-repo

Repo at the state of this blog post: https://github.com/danielpokladek/personal-shaders-repo/tree/62f59ac8e93a6c6b5149c4ecc380502bd708d251


Further Reading:

Catlike Coding – Looking Through Water (GrabPass): https://catlikecoding.com/unity/tutorials/flow/looking-through-water/

Harry Alisavakis’s take on GrabPass: https://halisavakis.com/my-take-on-shaders-grabpass/

Alan’s Impossible Geometry: https://www.alanzucconi.com/2015/12/09/3873/

Unity’s Surface Shader Examples: https://docs.unity3d.com/Manual/SL-SurfaceShaderExamples.html

Unity’s Vertex and Fragment Shader Examples: https://docs.unity3d.com/Manual/SL-VertexFragmentShaderExamples.html

Daniel Ilett’s Spyro Portal using Cubemap Textures: https://danielilett.com/2019-12-11-tut4-1-spyro-skyboxes/

Unity Manual on Stencil Shaders: https://docs.unity3d.com/2019.3/Documentation/Manual/SL-Stencil.html

Red Owl Games on Stencil Testing in Unity: https://www.redowlgames.nl/2017/01/18/shader-selftuition-stencil-testing-in-unity/

Unity Manual – Render Queue: https://docs.unity3d.com/ScriptReference/Material-renderQueue.html

Unity Manual – Cubemap Textures: https://docs.unity3d.com/Manual/class-Cubemap.html


logo-black-transparent

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Back to top