Developers Club geek daily blog

1 year, 6 months ago
In this article I will tell about how to reach here such effect:

We create 2D - portals by means of shaders

In fact, the shader about which the speech will go works as post-effect for the camera or the built-in blur and vignette filters in Unity. It accepts the input image (more precisely, RenderTexture) and brings him with the imposed effects.


Everything began with game for the thirtieth Ludum Dare game jam on Connected Worlds (The integrated worlds). The idea was following: two characters are on different sides of the screen separated into two identical speak rapidly, and send each other signals. Many players could not understand this mechanics therefore I slightly changed it.

I made so that, getting to a portal, the signal materialized in the parallel world – on other part of the screen. At the same time for work of a portal I thought up several options, for example, that at hit in it characters were interchanged the position. But all this was unclear and confused even more.

I long puzzled over this problem, but it would be too difficult to configure each moving object. Then I decided that for this purpose it is necessary to write a shader.
In fact, the shader about which the speech will go works as post-effect for the camera or the built-in blur and vignette filters in Unity. It accepts the input image (more precisely, RenderTexture) and brings him with the imposed effects.

1. We configure a shader and post-effects

Let's begin with the least important post-effect, just to test this configuration. First, we create the camera, having left the majority of default arguments:

We create 2D - portals by means of shaders

It is the most important to change the Clear Flags parameter (that when rendering the screen was not updated), to switch the camera to the orthographic mode and to set value of depth above, than for other cameras (to put the camera of the last plotting in queue). Then we write a new script (PortalEffect.cs) with such source code:

using UnityEngine;
using UnityStandardAssets.ImageEffects;
    [ExecuteInEditMode]
    [RequireComponent(typeof (Camera))]
    public class PortalEffect : PostEffectsBase
    {
        private Material portalMaterial;
        public Shader PortalShader = null;
        public override bool CheckResources()
        {
            CheckSupport(false);
            portalMaterial = CheckShaderAndCreateMaterial(PortalShader, portalMaterial);
            if (!isSupported)
                ReportAutoDisable();
            return isSupported;
        }

        public void OnDisable()
        {
            if (portalMaterial)
                DestroyImmediate(portalMaterial);
        }
        public void OnRenderImage(RenderTexture source, RenderTexture destination)
        {
            if (!CheckResources() || portalMaterial == null)
            {
                Graphics.Blit(source, destination);
                return;
            }
            Graphics.Blit(source, destination, portalMaterial);
        }
}


Now we create a new shader of PortalShader.shader with the following code:

Shader "VividHelix/PortalShader" {
    Properties {
  _MainTex ("Base (RGB)", 2D) = "white" {}
 }
    SubShader {
        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            uniform sampler2D _MainTex;
            
            struct vertOut {
                float4 pos:SV_POSITION;
            };
            vertOut vert(appdata_base v) {
                vertOut o;
                o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
                return o;
            }
            fixed4 frag(vertOut i) : SV_Target {
                return fixed4(.5,.5,.5,.1);
            }
            ENDCG
        }
    }
}


Having created a shader, do not forget to set it in PortalShader property of a script PortalEffect.
Here so the screen before activation of effect looks:

We create 2D - portals by means of shaders

And so – after activation:

We create 2D - portals by means of shaders

Gray color appears from behind a line fixed4(.5,.5,.5,.1) also consists of 50% red, green, blue and alphas with value 1.

2. We add UV coordinate

Now we will add to a UV coordinate shader. Their values can vary in the range from 0 to 1. It is the simplest to provide that this effect is imposed on the quadrangle executed by the screen size with the texture drawn by the previous cameras.

Following fragment of a code:

struct vertOut {
    float4 pos:SV_POSITION;
    float4 uv:TEXCOORD0;
};
vertOut vert(appdata_base v) {
    vertOut o;
    o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
    o.uv = v.texcoord;
    return o;
}
fixed4 frag(vertOut i) : SV_Target {
    return tex2D(_MainTex, 1-i.uv);
}


Thus, we overturn the image in vertical direction and horizontals twice that corresponds to turn by 180 degrees:

We create 2D - portals by means of shaders

Pay attention to a piece 1-i.uv. If to reduce it to i.uv, we will gain so-called "identical" effect which leaves the source image without changes. Line return tex2D(_MainTex, float2(1-i.uv.x,i.uv.y)); will just invert the image in horizontal direction (from left to right):

We create 2D - portals by means of shaders

3. We transfer area of the screen

We can modify a little a shader, having changed values of UV coordinates to impose a certain area on other section of the screen.

fixed4 frag(vertOut i) : SV_Target {
    float2 newUV = float2(i.uv.x, i.uv.y);
    if (i.uv.x < .25){
        newUV.x = newUV.x + .5;
    }
    return tex2D(_MainTex, newUV);
}


We create 2D - portals by means of shaders

On a screenshot you can see how section on the left part of the screen is copied from right. The size of this section can be adjusted, having changed value.25. We also add.5 that the image moved to opposite part of the screen – with 0-0.25 to 0.5-0.75 on a x axis.

4. We transfer circular area

Similarly to transfer circular area, we will add function of distance:

if (distance(i.uv.xy, float2(.25,.75)) < .1){
    newUV.x = newUV.x + .5;
}


We create 2D - portals by means of shaders

As you see, instead of a circle at us the oval turned out. The problem is that width and height of the screen unequal (we calculate distance in the range of 0-1). Height of an oval is equal to 20% of screen height, and width – 20% of its width (proceeding from value of radius of.1 or 10%).

5. We transfer circular area again

To solve this problem, we have to rewrite function of distance taking into account width and height of the screen.

fixed4 frag(vertOut i) : SV_Target {
    float2 scrPos = float2(i.uv.x * _ScreenParams.x, i.uv.y * _ScreenParams.y);
    if (distance(scrPos, float2(.25 * _ScreenParams.x,.75 * _ScreenParams.y)) < 50){
        scrPos.x = scrPos.x + _ScreenParams.x/2;
    }
    return tex2D(_MainTex, float2(scrPos.x/_ScreenParams.x, scrPos.y/_ScreenParams.y));
}


We create 2D - portals by means of shaders

6. We interchange the position of areas

To complete double replacement, we need to transfer similar area to the right half of the screen:

if (distance(scrPos, float2(.25 * _ScreenParams.x,.75 * _ScreenParams.y)) < 50){
    scrPos.x = scrPos.x + _ScreenParams.x/2;
}else if (distance(scrPos, float2(.75 * _ScreenParams.x,.75 * _ScreenParams.y)) < 50){
    scrPos.x = scrPos.x - _ScreenParams.x/2;
}


Here that has to turn out:

We create 2D - portals by means of shaders

7. We add indistinct edges

Now transition looks rather sharply therefore we need to blur edges a few. For this purpose we use linear interpolation.

At first everything is simple:


float lerpFactor=0;
if (distance(scrPos, float2(.25 * _ScreenParams.x,.75 * _ScreenParams.y)) < 50){
    scrPos.x = scrPos.x + _ScreenParams.x/2;
    lerpFactor = .8;
}else if (distance(scrPos, float2(.75 * _ScreenParams.x,.75 * _ScreenParams.y)) < 50){
    scrPos.x = scrPos.x - _ScreenParams.x/2;
    lerpFactor = .8;
}
return lerp(tex2D(_MainTex, i.uv), tex2D(_MainTex, float2(scrPos.x/_ScreenParams.x, scrPos.y/_ScreenParams.y)), lerpFactor);


This code "will blur" regions of the moved areas, using 80% (that corresponds to 0.8) the moved pixels:

We create 2D - portals by means of shaders

Now let's make transition by smoother by means of function of distance (instead of executing double replacement, we meanwhile will concentrate on one area).

float lerpFactor=0;
float2 leftPos = float2(.25 * _ScreenParams.x,.75 * _ScreenParams.y);
if (distance(scrPos, leftPos) < 50){
    lerpFactor = (50-distance(scrPos, leftPos))/50;
    scrPos.x = scrPos.x + _ScreenParams.x/2;
}   
return lerp(tex2D(_MainTex, i.uv), tex2D(_MainTex, float2(scrPos.x/_ScreenParams.x, scrPos.y/_ScreenParams.y)), lerpFactor);



As you see, it works, but demands additional setup:

We create 2D - portals by means of shaders

8. Blur of edges with effect of vignetting

For a solution of this problem I suggest to go an alternate path. Let's say we want to blur only outline border with thickness 15. It means that for distances 35 and less than a coefficient of linear interpolation has to be equal 1, and for distance 50 – this coefficient has to be equal to zero. In if branch the distance is specified in the range from 0 to 50. So, to display a final formula, we will make the small plate:

We create 2D - portals by means of shaders

The saturate function is equal to clamp (0,1) (transforming negative values in 0).
Using a final formula lerpFactor = 1-saturate((distance(scrPos, leftPos)-35)/15), we receive such result:

We create 2D - portals by means of shaders

Here so the complete code for blur of edges of two areas looks:

float2 leftPos = float2(.25 * _ScreenParams.x,.75 * _ScreenParams.y);
float2 rightPos = float2(.75 * _ScreenParams.x,.75 * _ScreenParams.y);
if (distance(scrPos, leftPos) < 50){
    lerpFactor = 1-saturate((distance(scrPos, leftPos)-35)/15);
    scrPos.x = scrPos.x + _ScreenParams.x/2;
} else if (distance(scrPos, rightPos) < 50){
    lerpFactor = 1-saturate((distance(scrPos, rightPos)-35)/15);
    scrPos.x = scrPos.x - _ScreenParams.x/2;
}


We create 2D - portals by means of shaders

9. We configure shader settings

Our shader is almost ready, but with hardcoded-values from it I pound a little. We can retrieve them in parameters of a shader and change by means of a code.

After extraction the final code of a shader looks so:

Shader "VividHelix/PortalShader" {
    Properties {
  _MainTex ("Base (RGB)", 2D) = "white" {}
        _Radius ("Radius", Range (10,200)) = 50
  _FallOffRadius ("FallOffRadius", Range (0,40)) = 20
        _RelativePortals ("RelativePortals", Vector) = (.25,.25,.75,.75)
 }
    SubShader {
        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            uniform sampler2D _MainTex;
            uniform half _Radius;
            uniform half _FallOffRadius;
            uniform half4 _RelativePortals;
            
            struct vertOut {
                float4 pos:SV_POSITION;
                float4 uv:TEXCOORD0;
            };

            vertOut vert(appdata_base v) {
                vertOut o;
                o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
                o.uv = v.texcoord;
                return o;
            }

            fixed4 frag(vertOut i) : SV_Target {
                float2 scrPos = float2(i.uv.x * _ScreenParams.x, i.uv.y * _ScreenParams.y);
                float lerpFactor=0;
                float2 leftPos = float2(_RelativePortals.x * _ScreenParams.x,_RelativePortals.y * _ScreenParams.y);
                float2 rightPos = float2(_RelativePortals.z * _ScreenParams.x,_RelativePortals.w * _ScreenParams.y);
                if (distance(scrPos, leftPos) < _Radius){
                    lerpFactor = 1-saturate((distance(scrPos, leftPos) - (_Radius-_FallOffRadius)) / _FallOffRadius);
                    scrPos.x = scrPos.x + rightPos.x - leftPos.x;
                    scrPos.y = scrPos.y + rightPos.y - leftPos.y;
                } else if (distance(scrPos, rightPos) < _Radius){
                    lerpFactor = 1-saturate((distance(scrPos, rightPos)- (_Radius-_FallOffRadius)) / _FallOffRadius);
                    scrPos.x = scrPos.x + leftPos.x - rightPos.x;
                    scrPos.y = scrPos.y + leftPos.y - rightPos.y;
                }
                return lerp(tex2D(_MainTex, i.uv), tex2D(_MainTex, float2(scrPos.x/_ScreenParams.x, scrPos.y/_ScreenParams.y)), lerpFactor);
            }
            ENDCG
        }
    }
}


Standard (asymmetric) values yield to us such result:

We create 2D - portals by means of shaders

In our case parameters of a shader can be set in PortalEffect.cs:


public void OnRenderImage(RenderTexture source, RenderTexture destination)
{
    if (!CheckResources() || portalMaterial == null)
    {
        Graphics.Blit(source, destination);
        return;
    }

    portalMaterial.SetFloat("_Radius", Radius);
    portalMaterial.SetFloat("_FallOffRadius", FallOffRadius);
    portalMaterial.SetVector("_RelativePortals", new Vector4(.2f, .6f, .7f, .6f)); 
    Graphics.Blit(source, destination, portalMaterial);
}  


10. Finishing touches

Even with effect of vignetting transition does not look as it would be desirable. It can be corrected, having added to a portal something like a bordering. In older version of a code for this purpose I used a particle system:

We create 2D - portals by means of shaders
We create 2D - portals by means of shaders
We create 2D - portals by means of shaders

Having cardinally changed style of game, I used a shader of "walls on fire" (the burning walls) for rendering of normal round sprayt around portals. Considering that process of rendering happens before portals are interchanged the position, this effect looks quite abruptly:

We create 2D - portals by means of shaders
We create 2D - portals by means of shaders
We create 2D - portals by means of shaders

11. End result

Here some more gif images demonstrating the end result at work:

We create 2D - portals by means of shaders
We create 2D - portals by means of shaders

This article is a translation of the original post at habrahabr.ru/post/274509/
If you have any questions regarding the material covered in the article above, please, contact the original author of the post.
If you have any complaints about this article or you want this article to be deleted, please, drop an email here: sysmagazine.com@gmail.com.

We believe that the knowledge, which is available at the most popular Russian IT blog habrahabr.ru, should be accessed by everyone, even though it is poorly translated.
Shared knowledge makes the world better.
Best wishes.

comments powered by Disqus