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:




Tuesday 9 January 2018

Tilemap: custom brush for prefabs.

Finally, a new post.

I haven't been using Unity much recently as I got sucked into OpenGL development, however, with the recent updates that came to Unity I was drawn back to it.

Being a big fan of 2D, I was excited to see the new features added, in particular, the tilemap system: a very easy and fast way to lay out 2D maps and create levels.

I then started messing around with custom brushes and I came up with a slightly modified version of the prefab brush that is provided in this tutorial project. I modified the PrefabBrush script in order to be able to select different "prefab palettes" and display the preview in the scene editor.

Also, for this tutorial I re-used this asset you have seen before.

To set it up, I created 4 prefabs in 2 different folders "Collectibles" and "Enemies". I have a "Coin" and a "Heart" prefab in the "Collectibles" folder and a "Slime" and "Bat" prefab in the "Enemies" folder. These prefabs are going to be GameObjects that we will place in the scene using the brush we are creating. The brush will allow you to choose which prefab palette you wish to use and iterate through the GameObjects of that particular palette.

Firstly we need to create the template for the prefab palette: it does not get any easier that this script down here:


[CreateAssetMenu]
public class PrefabPalette : ScriptableObject {
 public GameObject[] prefabs;

}

I kid you not, that is all you need.

This script will enable you to create an asset, which is really a file in which you can place your prefabs:



You can now create 2 prefab palettes, one for the enemies, one for the collectables.

We can now take a look at the PrefabBrush script. I will highlight the modifications I made. It is important to notice how the script is placed inside a folder called "Editor", that is because it incorporates editor scripting as well and it must be inside that folder.


  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


namespace UnityEditor
{
 [CreateAssetMenu]
 [CustomGridBrush(false, true, false, "Prefab Brush")]
 public class PrefabBrush : GridBrush
 {

  public PrefabPalette prefabPalette;

  [HideInInspector] public int index = 0;

  public override void Paint(GridLayout grid, GameObject brushTarget, Vector3Int position)
  {   

   GameObject prefab = prefabPalette.prefabs[index];
   GameObject instance = (GameObject) PrefabUtility.InstantiatePrefab(prefab);
   Undo.RegisterCreatedObjectUndo((Object)instance, "Paint Prefabs");
   if (instance != null)
   {
    instance.transform.SetParent(brushTarget.transform);
    instance.transform.position = grid.LocalToWorld(grid.CellToLocalInterpolated(new Vector3Int(position.x, position.y,0) + new Vector3(.5f, .5f, .5f)));
   
   }
  }

  public override void Erase(GridLayout grid, GameObject brushTarget, Vector3Int position)
  {
   // Do not allow editing palettes
   if (brushTarget.layer == 31)
    return;

   Transform erased = GetObjectInCell(grid, brushTarget.transform, new Vector3Int(position.x, position.y, position.z));
   if (erased != null)
    Undo.DestroyObjectImmediate(erased.gameObject);
  }

  private static Transform GetObjectInCell(GridLayout grid, Transform parent, Vector3Int position)
  {
   int childCount = parent.childCount;
   Vector3 min = grid.LocalToWorld(grid.CellToLocalInterpolated(position));
   Vector3 max = grid.LocalToWorld(grid.CellToLocalInterpolated(position + Vector3Int.one));
   Bounds bounds = new Bounds((max + min)*.5f, max - min);

   for (int i = 0; i < childCount; i++)
   {
    Transform child = parent.GetChild(i);
    if (bounds.Contains(child.position))
     return child;
   }
   return null;
  }
 
 }

 [CustomEditor(typeof(PrefabBrush))]
 public class PrefabBrushEditor : GridBrushEditor
 {
  private PrefabBrush prefabBrush { get { return target as PrefabBrush; } }
   
  GameObject holder;

  public override void OnMouseLeave ()
  {
   base.OnMouseLeave (); 

   if(holder)
    DestroyImmediate (holder);
  }

 
  public override void OnPaintSceneGUI (GridLayout gridLayout, GameObject brushTarget, BoundsInt position, GridBrushBase.Tool tool, bool executing)
  {

   base.OnPaintSceneGUI (gridLayout, null,position, tool, executing); 

   Event e = Event.current;

   if (e.type == EventType.KeyDown && e.keyCode == KeyCode.C) {
    //Debug.Log ("Click, index " + prefabBrush.index);

    prefabBrush.index++;
    if (prefabBrush.index == prefabBrush.prefabPalette.prefabs.Length)
     prefabBrush.index = 0;

    DestroyImmediate (holder);
    holder = Instantiate (prefabBrush.prefabPalette.prefabs [prefabBrush.index]);

   }

   if (!holder)
    holder = Instantiate (prefabBrush.prefabPalette.prefabs [prefabBrush.index]);

   holder.transform.position = position.position + new Vector3(.5f,0.5f,0.0f);

  }  

 }
}

Up to line 53 is the "normal" script, the actual PrefabBrush class. From line 53 onward is the custom editor script.

The PrefabBrush class is pretty much the same as it was in the original script. The method Pain is overridden in order to instantiate the currently selected prefab. This prefab is chosen according to an int value, which is the index of the array of GameObjects found in the PrefabPalette public object. The other 2 methods are straight from the original script and override erase functionality in order to delete the instantiated prefab rather than the underneath tiles that might be present in the tilemap.

The custom editor part is where I put my hands mostly. In order to show the "preview" of the prefab in the scene editor I used a trick: I instantiate an object of the selected prefab (the object holder in the script) and I constantly update its position. OnMouseLeave is called whenever the mouse leaves the editor window. In this method, I delete the holder gameobject so it is no longer present in the scene.

The method OnPaintSceneGUI is continuously called as long as the mouse is in the scene editor window. Here I do a couple of things: I check that the key "C" is pressed: if so, the value of index is incremented (and checked for boundaries), so the prefab will change. If that occurs, I delete the current holder and I create a new one with the new selected prefab from the prefab palette.

I also create a holder object anyway (line 90-91), as the "preview" of the GameObject must be present even if the key "C" is not pressed. I finally update the position of the holder object to follow the value of position which is the cell in the tilemap the mouse is currently hovering on.

At this point, we simply need to create a Prefab Brush object in the project folder and it will then be available in the Tile Palette window:




We can then select the prefab palette we created and start placing prefabs!






Conclusion

The new tilemap feature is fantastic if you are planning to create a 2D game and quickly need to draft levels.

It is important to remember:

- Always separate different tiles/prefabs in different tilemaps: just like in the example above, enemies prefabs are placed in a different tilamap than collectibles.

- Make sure to select the actual tilemap you wish to draw on in the Tile Palette window:


It's easy to forget that and place all the tiles/prefabs in one single tilemap.

- Check the tool you are using: occasionally, the "erase" tool remains selected and it's easy to get confused on "why is it not painting??". Silly mistakes, but can happen.