Blending and Translating Two Textures in an Unlit Shader in Unity

Texture Blending transitions

Texture Blending transitions

Above is a demo. Just use the slider to transition between the two backgrounds. If you cannot see the demo you may want to try a different browser.

Problem Summary

Unity version: Unity 2020.1.5.f1

In my game, each level is split into two parts with a mid-level and end of level boss. The mid-level boss is supposed to be fairly easy and perhaps a bit dumb simply to give the player a bit of a breather. The second half of each level is another ramp up in pace and enemy types. I wanted the user to know that they’d progressed by the fact that the background had also changed. What I wanted to happen was for the initial scrolling background to fade out and a new scrolling background to fade in. It would work perfectly for fading between the colours of nebulas. Do do this I’ve used an unlit shader in Unity with two textures that can be programmatically modified from outside of the shader code.

Introduction

I am going to be the first to admit that I know almost nothing about Unity Shader or more specifically, the Shader language which looks like pseudo C. This seems to work for me but if someone has a better, more efficient way of producing similar results I’d love to hear it.

Tutorial

I’m starting with a Quad mesh to hold my material:

Right-click in the Unity Hierarchy and select 3D Object->Quad from the resulting menus

In the Inspector, click the vertical ellipses (the three dots …) at the top of the Transform section and selected Reset Position

The Quad comes with a Mesh Renderer, a Mesh Collider and a default material.

My game is 2D and I’m using an orthographic camera so the distance for the background and other objects doesn’t really matter. The game is a vertical shooter with the player ship typically as the bottom of the screen and enemies dropping down from the top. To give the illusion of travel the Quad is twice the height of the viewport and the wrap mode is set to Repeat on each of the background textures. So the background will automatically circle around as time goes on once the BackgroundScroller script is written. You may have other requirements so set your sizes and texture properties as you see fit.

Properties for a Game Background Image

With the Quad selected in the Scene, you can simply drop a suitable texture straight onto it. This will replace the default material and likely end with an incorrect shader for your needs. For my game, it’s in space and so it shouldn’t take on any lighting so an Unlit Shader is perfect for this job.

Right-click inside an appropriate folder inside Assets and select Create->Shader->Unlit Shader

Name the shader something appropriate such as BackgroundBlendShader

Double click the newly created Shader to open it up in your IDE, for me that’s Visual Studio

Here’s what the current version creates for us.


Shader "Unlit/BackgroundBlendShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" 
    
    SubShader
    {
        Tags   
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}

In the Properties section I made the following modifications:



Properties
{
  _MainTex("Background One", 2D) = "white" {}
  _SecondaryTex("Background Two", 2D) = "white" {}
  _Blend("Blend Amount", Range(0.0,1.0)) = 0.0
}

_MainTex is the default property that holds a texture such as the initial game background we want to display in the game. “Background One” is the display name as the texture will eventually be referred to in Unity’s Material property in the Quad. You can name it however you want as long as it makes sense when you get back to Unity. To use another texture I simply replicated the _MainTex property and named it _SecondaryTex with the display name of “Background Two”.

The _Blend property allows us to control the amount of blending between the first texture and the second texture using Range() values between 0.0 and 1.0 where 0.0 displays 100% of the first texture and 1.0 displays 100% of the second texture. The default value is set to 0.0.

I made the following modifications to the v2f structure:


struct v2f
{
  float2 uv : TEXCOORD0;
  float2 uv2 : TEXCOORD1;
  UNITY_FOG_COORDS(2)
  float4 vertex : SV_POSITION;
};

The v2f or Vertex Shader to Fragment Shader structure stores Vertex Shader derived data in readiness for per-pixel calculations in the Fragment Shader. We need to add a variable to store the second textures coordinates (float2 uv2 : TEXCOORD1:) as well as increase the number of coordinates values for UNITY_FOG_COORDS() function to handle to 2.

There is some useful information at the Unity3D Wiki if you’re interested to learn more about Shader properties and code.

Next, I need to add variables to store the secondary texture and blend amount to link the properties to the code:


sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _SecondaryTex;
float4 _SecondaryTex_ST;
float _Blend;

I simply replicated the _MainTex variables and named them _SecondaryTex. I also added a _Blend float variable to link to our _Blend property. I don’t think I needed to replicate the _MainTex_ST because I never needed to use it but I added it for code consistency.

Now we’re going to apply the _SecondaryTex variable to Vertex Shader coordinate system :


v2f vert (appdata v)
{
  v2f o;
  o.vertex = UnityObjectToClipPos(v.vertex);

  o.uv = TRANSFORM_TEX(v.uv, _MainTex);
  o.uv2 = TRANSFORM_TEX(v.uv, _SecondaryTex);
  UNITY_TRANSFER_FOG(o,o.vertex);

  return o;
}

By adding,

o.uv2 = TRANSFORM_TEX(v.uv, _SecondaryTex);

we’ve tied the second texture coordinate calculations into the Vertex Shader code which allows us to move both the first and second textures independently.

Now for the blending functionality:


fixed4 frag (v2f i) : SV_Target
{
  // sample the texture
  fixed4 col = lerp(tex2D(_MainTex, i.uv), tex2D(_SecondaryTex, i.uv2), _Blend);
  // apply fog
  UNITY_APPLY_FOG(i.fogCoord, col);
  return col;
}

Again, it’s a single line addition:

fixed4 col = lerp(tex2D(_MainTex, i.uv), tex2D(_SecondaryTex, i.uv2), _Blend);

Using the lerp() function with the _Blend variable we have control over the blending between the two textures at the Shader level.

Back in Unity, all being well, the Material should look something like this:

Quad Material using our Unlit Shader

Quad Material using our Unlit Shader

This particular version is directly from my game so there may be some minor differences.

Now we simply click Select on each of the above image thumbnails to apply our image backgrounds or drag and drop the images from the Assets folder.

Sliding the Blend Amount slider will demonstrate the blending mode between the two textures in the Game and Scene Viewports of Unity. Changing the Y Offset value on Background One and Two will move the texture up or down dependant on the value entered. As an example, set the Blend slider to around 50% and change each of the Y Offsets and you should see the results in the Game and Scene viewports.

Changing the Y Offset works for my vertical shooter but changing the X Offset would work better in a side scroller.

Now I’d like to tie this into my Unity code and I do this by adding a script to the Quad.

Click Add Component in the Inspector for the Quad and select New Script

Name it something appropriate, I called it BackgroundScroller

Here’s my code:


using UnityEngine;

public class BackgroundScroller : MonoBehaviour
{
  [SerializeField] private float _backgroundTransitionDuration = 20.2f;
  [SerializeField] private Vector2 _textureMaterial1Offset;
  [SerializeField] private Vector2 _textureMaterial2Offset;

  [SerializeField] private float _backgroundScrollSpeed = 0.025f;
  public float BackgroundScrollSpeed { get { return _backgroundScrollSpeed; } private set { _backgroundScrollSpeed = value; } }

  private MeshRenderer _backgroundMeshRenderer = null;
  private float _deltaTime = 0.0f;
  private Vector2 _offset;
  private float _backgroundTransitionState = 0.0f;

  public bool BackgroundTransitionRequested { get; set; }
  private void Start()
  {
    _offset = new Vector2(0f, _backgroundScrollSpeed);
    BackgroundTransitionRequested = false;
    _backgroundMeshRenderer = GetComponent();
  }

  private void Update()
  {
    _textureMaterial1Offset += _offset * Time.deltaTime;
    _textureMaterial2Offset += _offset * Time.deltaTime;
    _backgroundMeshRenderer?.sharedMaterial.SetTextureOffset("_MainTex", _textureMaterial1Offset);
    _backgroundMeshRenderer?.sharedMaterial.SetTextureOffset("_SecondaryTex", _textureMaterial2Offset);

    if (BackgroundTransitionRequested && _backgroundTransitionState < 1.0f)
    {
      _deltaTime += Time.deltaTime;
      _backgroundTransitionState = _deltaTime / _backgroundTransitionDuration;
      _backgroundTransitionState = Mathf.Clamp01(_backgroundTransitionState);
      _backgroundMeshRenderer?.material.SetFloat("_Blend", _backgroundTransitionState);
    }
  }
}

Essentially, I’ve exposed the following to the Unity UI:

  • The blending duration between the first and second textures through _backgroundTransitionDuration

  • The X and Y material offsets for each texture via the Vector2 _textureMaterial1Offset and _textureMaterial2Offset

  • The speed at which the texture offsets move via _backgroundScrollSpeed

    • This variable is exposed to other source code via the C# property BackgroundScrollSpeed

Both textures exist at all times whilst the game level is running and both are being moved (translated) by the offset via the calculations that occur during each Update() cycle. The speed of the translation is defined by the Unity exposed value Background Scroll Speed multiplied by the difference in time between now and the previous frame.

The blending is controlled via a source code controlled flag (bool) named BackgroundTransitionRequested which is defaulted to false. When another source code sets this flag to true the Update() cycle calculates the blend amount by dividing the difference in time from the last frame by the Background Transistion Duration value exposed in Unity. The resulting value is restricted to something between 0.0 and 1.0 via Mathf.Clamp01() and then applied to the Blend value exposed on the Quad material.

I think the results look pretty neat and help to let the player know that progress is being made in the game as well as help tell the story in a minor way.

Here’s an example of the full BlendBackgroundsShader.shader code I use in the game.


Shader "Unlit/BlendBackgroundsShader"
{
    Properties
    {
      _MainTex("Background One", 2D) = "defaulttexture" {}
      _SecondaryTex("Background Two", 2D) = "defaulttexture" {}
      _Blend("Blend Amount", Range(0.0,1.0)) = 0.0
    }
    SubShader
    {
        Tags   
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float2 uv2 : TEXCOORD1;
                UNITY_FOG_COORDS(2)
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            sampler2D _SecondaryTex;
            float4 _SecondaryTex_ST;
            float _Blend;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);

                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.uv2 = TRANSFORM_TEX(v.uv, _SecondaryTex);
                UNITY_TRANSFER_FOG(o,o.vertex);

                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = lerp(tex2D(_MainTex, i.uv), tex2D(_SecondaryTex, i.uv2), _Blend);
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}

 

In Summary

Using a transition shader is an effective way of switching between moving backgrounds. You could use it to switch between night and day cycles on a platformer game. The downside to this technique is that you need to load in and move two backgrounds throughout the game runtime even though only one is visible for the most part.