Sunday 4 March 2018

2D Dynamic water with refraction effect.

I was always curious about how to get that distorted effect you see on water surfaces, which is something I have recently learnt how to do in OpenGL. I managed to create a water surface from scratch which can reflect and refract the environment.

I now want to try a similar experiment in unity: water, which can refract objects behind it.
Additionally, we are going to make the water dynamic using a similar method we use in the "Perlin noise for ocean waves post".

First thing first, create an empty GameObject and add these components to it: a script called "Water2D", a MeshFilter, a MeshRenderer and an EdgeCollider2D.

Then create a material. You can call it whatever you want of course, "Water_Refractive" sounds ok to me. You can leave the standard shader for now as we will firstly get the mesh to work properly and then we will apply a refractive shader. Assign the material to the MeshRenderer of our GameObject:



Part 1: dynamic mesh generation

The mesh is going to be re-created every frame. Certainly not the best approach if performance is your priority, but this will allow us to change the number of vertices during runtime, which is what I wanted. The script, previously added to the GameObject, called "Water2D", looks like this:


  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
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Water2D : MonoBehaviour {

 public int topVerticesCount = 10;
 public float waterHeight = 10;
 public float waterSpeed = 1;
 public float density = 50;
 public float amplitude = 5;

 Mesh mesh;
 MeshFilter m_filter;
 EdgeCollider2D m_collider;
 List<Vector3> topVertices;

 void Start () {

  topVertices = new List<Vector3> ();
  m_filter = GetComponent<MeshFilter> ();
  m_collider = GetComponent<EdgeCollider2D> ();
  mesh = new Mesh ();
 
 } 

 void Update () {

  UpdateTop ();
  GenerateMesh ();

 }

 void GenerateMesh()
 {
  List<Vector3> meshVertices = new List<Vector3>();
  List<Vector3> bottomVertices = new List<Vector3>();

  List<int> meshTriangles = new List<int>();
  List<Vector2> meshUVs = new List<Vector2> ();

  //Add bottom vertices
  for (int i = 0; i < topVerticesCount; i++) {

   float y = topVertices [i].y + waterHeight;

   bottomVertices.Add (new Vector3 (topVertices [i].x,topVertices [i].y - y,topVertices [i].z));  
  
  } 

  int totalVertivesCount = topVertices.Count + bottomVertices.Count;
  int totalTriangles = totalVertivesCount - 2;

  for (int i = 0; i < totalTriangles/2; i++) {
  
   meshVertices.Add (topVertices [i]);
   meshUVs.Add (new Vector2 ((float)i/topVerticesCount, 1.0f));

   meshVertices.Add (bottomVertices [i+1]);
   meshUVs.Add (new Vector2 ((float)(i+1)/topVerticesCount, 0.0f));

   meshVertices.Add (bottomVertices [i]);
   meshUVs.Add (new Vector2 ((float)i/topVerticesCount, 0.0f));


   meshVertices.Add (topVertices [i]);
   meshUVs.Add (new Vector2 ((float)i/topVerticesCount, 1.0f));

   meshVertices.Add (topVertices [i+1]);
   meshUVs.Add (new Vector2 ((float)(i+1)/topVerticesCount, 1.0f));

   meshVertices.Add (bottomVertices [i+1]);
   meshUVs.Add (new Vector2 ((float)(i+1)/topVerticesCount, 0.0f));
  
  }

  for (int i = 0; i < meshVertices.Count; i++)
   meshTriangles.Add (i);

  mesh.vertices = meshVertices.ToArray ();
  mesh.triangles = meshTriangles.ToArray ();
  mesh.uv = meshUVs.ToArray ();

  mesh.RecalculateBounds ();
  mesh.RecalculateNormals ();
  mesh.RecalculateTangents();
  m_filter.mesh = mesh;

 

 }

 void UpdateTop()
 {

  //Generate top vertices
  topVertices.Clear ();
  if (topVerticesCount < 2)
   topVerticesCount = 2;
  for (float i = 0; i < topVerticesCount; i++) {
   float y =amplitude *  Mathf.PerlinNoise ((i + Time.time * waterSpeed) / density, (i + Time.time * waterSpeed) / density);
   topVertices.Add (new Vector3 (i, y, 0));
  }

  //Update edge collider
  Vector2[] paths = new Vector2[topVertices.Count];

  for (int i = 0; i < paths.Length; i++)
   paths [i] = new Vector2 (topVertices [i].x, topVertices [i].y);

  m_collider.points = paths;
 }
}

If you are not familiar with meshes and how to create them there are plenty of tutorials online, especially on the official Unity website. To give you a quick crash course, a mesh is a collection of vertices and triangles which are drawn using those vertices. In this approach, I firstly create a line of horizontal vertices (which i called "topVertices"). The minimum amount for the top vertices is 2 of course (line 98 - 99). I then apply Perlin noise to the y parameter of each vertex in order to get them to wave (lines 100 - 103). The edge collider is also updated according to these vertices so objects can collide with the waves.

In the GenerateMesh method I simply "triangulate" the vertices: I create a "bottom vertex" for each "top vertex" and create triangles. The bottom vertices are placed at a certain distance from their respective top vertices. This distance is a public parameter (waterHeight)which basically determines the water height.

If you apply a basic color to the material and press play, you should see this:


Part 1: refraction effect

Indeed, it's shader writing time.

To create the refraction effect I used a special type of map called DUDV map. If you perform a quick google search for it you will find many of these maps to download. Save one and import it as a new asset.

Also, before proceeding, create a simple sprite and place it in the scene. Any image will do, we'll use it to show the refraction effect. I used one of the available sprites in Unity and tinted it red:




At this point, the shader itself. The code is very much based on the "Glass effect shader", which can be found here. I modified just enough to use the map I wanted to and added parameters for altering the effect.



  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
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
Shader "FX/Custom Water Distortion" {
Properties {
 _Color ("Tint", Color) = (1,1,1,1)
 _DistortionStrength  ("Distortion", range (0,15)) = 1
 _DistortionSpeedX  ("Distortion Speed X", range (-5,5)) = 0
 _DistortionSpeedY  ("Distortion Speed Y", range (-5,5)) = 0
 _MainTextureSpeedX  ("Main texture speed X", range (-10,10)) = 0
 _MainTextureSpeedY  ("Main texture speed Y", range (-10,10)) = 0
 _MainText("Main texture",2D) = "white"{} 
 _DistMap ("Distortion map", 2D) = "white" {}
}

Category {
 
 Tags { "Queue"="Transparent" "RenderType"="Opaque" }

 SubShader {

  //Create a texture with all that is on the screen behin our mesh.
  // This texture is accessible via the parameter _GrabTexture
  GrabPass {       
   Name "BASE"  
   }
  
  Pass {
   Name "BASE"
   Tags { "LightMode" = "Always" }
   
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"

struct appdata_t {
 float4 vertex : POSITION;
 float2 texcoord: TEXCOORD0;
};

struct v2f {
 float4 vertex : POSITION;
 float4 uvgrab : TEXCOORD0;
 float2 uvdist : TEXCOORD1;
 float2 uvmain : TEXCOORD2;
};

float _DistortionStrength;
float _MainTextureSpeedX;
float _MainTextureSpeedY;

float4 _DistMap_ST;
float4 _MainText_ST;
float4 _Color;

v2f vert (appdata_t v)
{
 v2f o;
 o.vertex = UnityObjectToClipPos(v.vertex);
 #if UNITY_UV_STARTS_AT_TOP
 float scale = -1.0;
 #else
 float scale = 1.0;
 #endif
 o.uvgrab.xy = (float2(o.vertex.x, o.vertex.y * scale) + o.vertex.w) * 0.5;
 o.uvgrab.zw = o.vertex.zw;
 o.uvdist = TRANSFORM_TEX( v.texcoord, _DistMap );
 o.uvmain = TRANSFORM_TEX( v.texcoord, _MainText );
 return o;
}

sampler2D _GrabTexture;
float4 _GrabTexture_TexelSize;
float _DistortionSpeedX;
float _DistortionSpeedY;
sampler2D _DistMap;
sampler2D _MainText;

float4 frag( v2f i ) : COLOR
{
 // Distortion map sampling. 
 half2 dist = tex2D(_DistMap, float2(i.uvdist.x + _Time.x*_DistortionSpeedX,i.uvdist.y - _Time.x*_DistortionSpeedY)).rg; 
 float2 offset = dist * _DistortionStrength * _GrabTexture_TexelSize.xy ;

 //Apply distortion to the grab texture by modifying the uv coordinates
 i.uvgrab.xy = offset * i.uvgrab.z + i.uvgrab.xy;
  
 float4 col = tex2Dproj( _GrabTexture, UNITY_PROJ_COORD(i.uvgrab)); 
 float4 mainColor = tex2D(_MainText,float2(i.uvmain.x + _Time.x * _MainTextureSpeedX,i.uvmain.y + _Time.x *_MainTextureSpeedY));

 return col * _Color * mainColor;
}
ENDCG
  }
 }



}

}

Simply described, the shader samples the distortion map, which represents a series of two dimensional vectors and creates an offset (using those vector 2) which is used to then sample the _GrabTexture (line 81 - 87). This is how the refraction effect is created.

This shader has parameters that let you move the textures and increase or decrease the distortion effect. Also, you can apply a normal texture if you want to.

Apply the shader to the material and this is the final result:



Conclusion

Water effect with refraction. A nice effect to add to your 2D game.
Remember, collisions are also updated, which means that objects can collide with your water. Specifically, with the top of the water, as we used an EdgeCollider.

If you place an object in the scene, with a 2D collider and a rigidbody, you should see it colliding with the water: