A Look at Spyro’s Portals – Distorting the Background

This is a continuation of the previous post: A Look at Spyro’s Portal – Part 1

Final version of portal shader

Fixing Some Bugs

Now that we have our background rendering we can actually start applying some effects to our portal! But before we can actually proceed with that, if you have played the game you’d notice that there is a strange issue with our shader… the background is upside down! Let’s fix that first.

At first I wasn’t sure why this was happening, but I’ve come across a video by World of Zero on The Most Complicated Nothing Shader, where the same issue was happening and it was fixed, so let’s apply the same fix!

struct Input
{
    float4 vertex;
 
    //float4 screenPos;
    float4 grabUV;
};
 
sampler2D _GrabTexture;
 
void vert (inout appdata_full v, out Input o)
{
    UNITY_INITIALIZE_OUTPUT(Input, o);
    o.vertex = UnityObjectToClipPos(v.vertex);
    o.grabUV = ComputeGrabScreenPos(o.vertex);
}
 
void surf (Input IN, inout SurfaceOutputStandard o)
{
    //float4 grabPassTexture = tex2Dproj(_GrabTexture, IN.screenPos);
    float4 grabPassTexture = tex2Dproj(_GrabTexture, UNITY_PROJ_COORD(IN.grabUV));
 
    o.Albedo = grabPassTexture;
}

So the first change starts on line 5, we’re no longer using screenPos, instead we will be using a custom variable called grabUV; you can’t use a name like uv_GrabTexture, as this will make Unity think that you want a float2 UV coordinates, while in our case we require a float4 as it needs the UV information as well as the texture information.

Next inside the vertex function we need to calculate our grabUV variable, in order to do this we need to use a built in Unity function called ComputeGrabScreenPos, this computes the texture coordinates so that we can use it to sample our grab texture properly (notice that we’re using the clip space position as a parameter).

Finally inside of our surface function we can sample the texture by using tex2Dproj function, but in this case for the coordinate we want to use a macro called UNITY_PROJ_COORD, which will return a texture coordinate suitable for use in projected texture reads.

Now if we play the game, you’ll see that the texture is no longer upside down!


Let there be distortion!

Portal background distortion in Spyro Reignited Trilogy

In Spyro Reignited Trilogy portals have this distortion scrolling effect happening on the inner part of the background, as well as the background itself is rather quiet blurry; one of those things we can sort out with some Photoshop, the other will require some coding but after all that’s why you’re here.

I’m going to open Photoshop and apply some blur to my background texture, the amount is up to you and how much you want the background blurred, I’ve only applied just some light gaussian blur to the image and you can see the comparison below:

Left: No blur applied to the texture | Right: Texture with blur applied to it.

Before we start working on the distortion effect, we need a distortion texture; you can use the same texture as I have used, or you can create your own perlin noise texture for the portal. If you would like to use the one I have, you can find it on GitHub repo, or below.

Distortion texture
#pragma target 3.5
 
struct Input
{
    float4 vertex;
 
    float4 grabUV;
    float4 distortUV;
};
 
sampler2D _GrabTexture;
sampler2D _DistortTex;
float4 _DistortTex_ST;

First, we need to create a new property for our distortion texture. I’m going to use the bump texture I’ve shown you above. We want the property to be a 2D texture, I will call it _DistortTex. Next we need to create a sampler2D for our _DistortTex, just like we did with _GrabTexture.

Now that the property is ready, lets create a new variable of float4 in our Input called distortUV; the reason why I’m making it a float4, even though it’s a UV (normally float2s), is because I want to store UVs for two textures in one variable.. Two textures you say? Let’s look at the portal again!

If you look close at the distortion effect, you can notice that there are two textures; one is moving on the “X” axis (left/right) and one is moving on the “Y” axis (up/down) – if you’re Unreal user, then replace “Y” with “Z”.

If you look closely at the shader, you will notice there are two textures moving, one on “x” axis and one on “y” axis, both combined they create the final distortion effect. Let’s recreate it, and it’s actually easier than you might think!

Inside of our vertex function we can start chipping away on the animation, to do that we will use our distortUV variable; first we will use TRANSFORM_TEX to scale and offset our texture coordinates and as the parameters we will use texcoord and _DistortTex. Remember to plug this into the “xy” channels of our variable as those will represent the first texture and “zw” will represent the second texture.

Next, for “zw” we can just copy the “xy” value. We need to animate those UVs, because in shaders you don’t animate the texture itself but it’s UVs instead, and in order to do this we can add _Time.x to the “y” and “z” channels of distortUV; additionally we can create a new property called _DistortSpeed and multiply that by _Time.x so that we have more control over the animation speed.

Now we just need to sample the texture in our surface function, in order to do this we will use UnpackNormal function and as parameter we will use tex2D the rest remains just like normal texture sampling, but for the UVs we will “xy” for the first texture and “zw” for the second. We only want the “xy” values of the UnpackNormal function.

Next we want to combine those two into a new variable of float, and if we use this result as rgb values of the albedo, we will see how the two textures are moving and the effect in action without any other texture. This is a good way to test if the effect is working correctly.

Properties
{
    _DistortTex("Distortion Texture", 2D) = "white" {}
    _DistortSpeed("Distortion Scroll Speed", float) = 4
}
 
struct Input
{
    float4 vertex;
 
    float4 grabUV;
    float4 distortUV;
};
 
sampler2D _GrabTexture;
sampler2D _DistortTex;
float4 _DistortTex_ST;
 
float _DistortSpeed;
 
void vert (inout appdata_full v, out Input o)
{
    UNITY_INITIALIZE_OUTPUT(Input, o);
            
    o.vertex = UnityObjectToClipPos(v.vertex);
    o.grabUV = ComputeGrabScreenPos(o.vertex);
 
    o.distortUV.xy = TRANSFORM_TEX(v.texcoord, _DistortTex);
    o.distortUV.zw = o.distortUV.xy;
 
    o.distortUV.y += _DistortSpeed * _Time.x;
    o.distortUV.z += _DistortSpeed * _Time.x;
}
 
void surf (Input IN, inout SurfaceOutputStandard o)
{
    // We need to do the texture lookup twice, as the textures move at different speed
    //  so the textures need ot use correct UVs.
    float2 distortTexture = UnpackNormal(tex2D(_DistortTex, IN.distortUV.xy)).xy;
    float2 distortTexture2 = UnpackNormal(tex2D(_DistortTex, IN.distortUV.zw)).xy;
    float combinedDistortion = distortTexture + distortTexture2;
             
    float4 grabPassTexture = tex2Dproj(_GrabTexture, UNITY_PROJ_COORD(IN.grabUV));
 
    o.Albedo = float4(combinedDistortion, combinedDistortion, combinedDistortion, 1);
}
Two distortion textures combined and displayed as the albedo texture.

So now that we can see that the textures are working properly, we can use them to animate the UVs of our grab texture so that we can see the nice distortion texture; in order to do this we need to create a new variable called grabPassUV which will be equal to grabUV from our Input.

Now we want to multiply combinedDistortion by the grabUV value and add that back into the “xy” values of grabPassUV. This sounds confusing but this will animate the UVs of our grab pass texture and create the final effect that we want. If you save the shader, and tab back into Unity you will notice that the shader is a little broken; that is because our distortion is too strong and we need to tune it down a little.

We need to create a new property called _DistortAmount of type float, and now we want to multiply our distortionTexture and distortionTexture2 by the new property, but that will also result in quite strong effect, and to make it easier to edit, we can divide the _DistortAmount value by 100 so now instead of typing “0.7” we can just type in 7 giving us more control over the effect.

Now finally we can plug our grabPassUV value into the UNITY_PROJ_COORD function in our grab pass texture sample and set albedo back to grabPassTexture and voila! We have a distortion effect, feel free to explore with the values and textures.

Here are my settings:

Shader "DP/Spyro/PortalInner"
{
    Properties
    {
        _DistortTex("Distortion Texture", 2D) = "white" {}
        _DistortSpeed("Distortion Scroll Speed", float) = 2
        _DistortAmount("Distortion Amount", float) = 5
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200
 
        CGPROGRAM
        #pragma surface surf Standard fullforwardshadows vertex:vert
        #pragma target 3.5
 
        struct Input
        {
            float4 vertex;
 
            float4 grabUV;
            float4 distortUV;
        };
 
        sampler2D _GrabTexture;
        sampler2D _DistortTex;
        float4 _DistortTex_ST;
 
        float _DistortSpeed;
        float _DistortAmount;
 
        void vert (inout appdata_full v, out Input o)
        {
            UNITY_INITIALIZE_OUTPUT(Input, o);
             
            o.vertex = UnityObjectToClipPos(v.vertex);
            o.grabUV = ComputeGrabScreenPos(o.vertex);
 
            o.distortUV.xy = TRANSFORM_TEX(v.texcoord, _DistortTex);
            o.distortUV.zw = o.distortUV.xy;
 
            o.distortUV.y += _DistortSpeed * _Time.x;
            o.distortUV.z += _DistortSpeed * _Time.x;
        }
 
        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            // We need to do the texture lookup twice, as the textures move at different speed
            //  so the textures need ot use correct UVs.
            float2 distortTexture = UnpackNormal(tex2D(_DistortTex, IN.distortUV.xy)).xy;
            distortTexture *= _DistortAmount / 100;
            float2 distortTexture2 = UnpackNormal(tex2D(_DistortTex, IN.distortUV.zw)).xy;
            distortTexture2 *= _DistortAmount / 100;
            float combinedDistortion = distortTexture + distortTexture2;
             
            float4 grabPassUV = IN.grabUV;
            grabPassUV.xy += combinedDistortion * IN.grabUV;
 
            float4 grabPassTexture = tex2Dproj(_GrabTexture, UNITY_PROJ_COORD(grabPassUV));
 
            o.Albedo = grabPassTexture;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

You’ve made it, congratulations! This isn’t the end yet, we’ve got one more effect, the outline, to create and some additional things like smoke particles that will be covered in the next blog post!

Thank you for reading and I hope you find this useful. 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/af1977f2ed297983a8462749ae1d06c50698729a


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