Unity Shader: Spherical Mask

I'm a big fan of Beyond Eyes game and the way the world takes form while the little girl walks around.
Last time we looked at the dissolve effect this time we take a look at Spherical Mask effect.
The two effects combined together will let us create a custom shader that emulates the Beyond Eyes effect.
Let's begin!

Spherical Mask


The image above is pretty self explanatory, we have a player object that has a radius of action, every pixels inside its radius of action will be colored while the others stay in grayscale.
Let's begin with adding two input propeties to our shader:

1
2
3
4
5
6
7
8
9
Properties
{
 //...other shader properties
 
 //Spherical Mask properties
 _PlayerPosition("Player Position", Vector) = (0, 0, 0, 0)
 _Radius("Mask Radius", Range(0, 100)) = 0
 _Softness("Mask Softness", Range(0, 100)) = 0
}


  • _PlayerPosition passes the player postion to the shader 
  • _Radius is the player radius of action, anything inside of it will be colored
  • _Softness controls how soft is the transition between grayscaled pixels and the colored ones
Now in the surf function we'll do the following things:
  • Sample the color of the texture before making it grayscaled
  • Compute the color of the texture in grayscale so that by default the texture will appear in grayscale
  • For any vertex of the plane, compute its distance from the player
  • Now that we have the distance, check if the vertex is inside the player radius of action
  • Apply the softness
  • For every pixel, lerp between the grayscale texture to the colored one by how near is the vertex to the player

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void surf (Input IN, inout SurfaceOutputStandard o)
{
 //Get the color of the texture
 fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;

 //Compute the color of the texture in grayscale
 float grayscale = (c.r + c.g + c.b) / 3;
 fixed3 c_gray = fixed3(grayscale, grayscale, grayscale);

 //For any vertex of the plane, compute its distance from the player 
 half d = distance(_PlayerPosition, IN.worldPos);
 //Check if the vertex is inside the player radius of action
 half near_value = d - _Radius;
 //Apply the softness
 near_value = near_value / -_Softness;
 //Clamp the near_value_soft to a range (0, 1) 
 near_value = saturate(near_value);
 //Lerp from grayscale to the colored texture by the amount of near_value
 fixed4 lerpColor = lerp(fixed4(c_gray, 1), c, near_value);

 //Apply the lerpColor
 o.Albedo = lerpColor.rgb;
 o.Metallic = _Metallic;
 o.Smoothness = _Glossiness;
 o.Alpha = c.a;

 // Metallic and smoothness come from slider variables
 o.Albedo = lerpColor.rgb;
 o.Metallic = _Metallic;
 o.Smoothness = _Glossiness;
 o.Alpha = c.a;
}

Here's the final code:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
Shader "Mistwork/Spherical Mask"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _Metallic ("Metallic", Range(0,1)) = 0.0
        //Spherical Mask properties
        _PlayerPosition("Player Position", Vector) = (0, 0, 0, 0)
        _Radius("Mask Radius", Range(0, 100)) = 0
        _Softness("Mask Softness", Range(0, 100)) = 0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        // Physically based Standard lighting model, and enable shadows on all light types
        #pragma surface surf Standard fullforwardshadows

        // Use shader model 3.0 target, to get nicer looking lighting
        #pragma target 3.0

        sampler2D _MainTex;

        struct Input
        {
            float2 uv_MainTex;
            //The object world-space position to which this shader is applied
            float3 worldPos;
        };

        half _Glossiness;
        half _Metallic;
        fixed4 _Color;
        float4 _PlayerPosition;
        half _Radius;
        half _Softness;

        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            //Get the color of the texture
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;

            //Compute the color of the texture in grayscale
            float grayscale = (c.r + c.g + c.b) / 3;
            fixed3 c_gray = fixed3(grayscale, grayscale, grayscale);

            //For any vertex of the plane, compute its distance from the player 
            half d = distance(_PlayerPosition, IN.worldPos);
            //Check if the vertex is inside the player radius of action
            half near_value = d - _Radius;
            //Apply the softness
            near_value = near_value / -_Softness;
            //Clamp the near_value_soft to a range (0, 1) 
            near_value = saturate(near_value);
            //Lerp from grayscale to the colored texture by the amount of near_value
            fixed4 lerpColor = lerp(fixed4(c_gray, 1), c, near_value);

            //Apply the lerpColor
            o.Albedo = lerpColor.rgb;
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = c.a;

            // Metallic and smoothness come from slider variables
            o.Albedo = lerpColor.rgb;
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = c.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

How to pass the Player position to the shader

First, attach our Spherical Mask shader to the object we want to get colored as the player moves.
To pass the player position to the shader we use a pretty simple C# code that we have to attach to the same object.
Here's the code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SphericalMask : MonoBehaviour
{
    [SerializeField]
    private GameObject player;

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        GetComponent<Renderer>().material.SetVector("_PlayerPosition", player.transform.position);
    }
}

Line 19 get a reference to our shader from the material of our object and pass the player position (player.transform.position).

Comments

Popular posts from this blog

GLSL Shader in Maya Part 1

Unity Shader: Toon Water Shader

Unity Shader: Spherical Mask Dissolve