Blobby things, Bezier Curves and Shaders(Part I)

At the last Global Game Jam (2016) I worked on a 2D physics puzzle game , and we wanted to have monsters which were like jello-o blobs of lava which you could interact with physically, and since we had some really good artists with us, we decided to implement them as spritesheet animations with some animation state-machine controlling code.

This didn't work out too well and we constantly ended up with situations that looked a bit like this.


Whats happening here is the state-machine script keeps switching the animation state between the falling and the resting animation as the collider bounces of the wall on the left and the platform on the right, and no amount of tweaking and rewriting the controller script with more conditions seemed to make a difference. 

The correct way to go about it would be would be to physically simulate the blob, but Unity really doesn't provide a way to do this in a way that doesn't look terrible and allows the artist to add some character to the blob ( can you spot the facial expressions in the gif above? ), I have some solutions in my head to tackle this and this blog post is the first in a series in which I try an find a one.

Step I : Setup the joints for the blob physics simulation

This is fairly straightforward in Unity, you basically want to set up a bunch of Rigibody2D's and SpringJoint2D's that look a bit like this,

Each of the green lines is one  SpringJoint2D and each square is a Rigidbody2D with a BoxCollider2D. ( Make sure all of the physics objects are in the same level and do NOT child them among each other )

Step II : Rendering the Blob

If we were to simply build up  triangles along the spring joints and fill up the inside, you get something that looks like this,
That behavior is pretty blobby, but its too jagged because we don't have enough triangles. Well, the easy solution to this is to just add a bunch more points on the outer loop, but this will slow down performance a lot, especially on mobile, especially if you have a lot of them simulating at the same time, we can do better.

We effectively want to round the corners on the blob above, and we want to let the GPU do this work for us, because CPU  bound curve smoothing code in C# would put us squarely back into to same problem of slowdown on mobile as we would have to compute a LOT of intermediary points to make it look smooth.

So, we want to apply a curve smoothing algorithm (Bezier Curves/B-Splines/ Catmull-Rom) on the GPU, while pretty much any smoothing algorithm can be implemented on the GPU using Geometry Shaders ,with that we would only be able to support OpenGL4+ or DX11+ devices, not ideal since a large portion of current mobile devices( and even WebGL) are stuck on OpenGL ES 2.1 and  only support Vertex and Fragment Shaders. However, Loop & Blinn at Microsoft Research  figured out how to do bezier curve rendering on fragment shaders at the per pixel level, and used it for creating resolution-independent texture maps of text on 3D surfaces, here's a really cool result from their paper.

The Math

I'll skip over the really Math-y bits in the paper, and get right to the final result that's useful for us, for the triangle given below,

with [u, v] co-ordinates of the vertices as shown, a point( x,y) is inside the curve  (colored red) if ( y * y - x ) < 0. 

This gives us a simple condition that we can evaluate at the per-pixel level of our fragment shader, if you want to see why you can go read the paper for their derivation or refer this GPU Gems article by Nvidia.

However, since we are restricted to quadratic Bezier Curves, we are going to have to change our triangulation scheme significantly to support this shader for the result we need.

Triangulation Scheme

We basically want our triangles to look like this,


What we're doing here is between each of the outer rigidbodies, we add TWO vertices at the mid-point, this is because,
  1. Two separate bezier curves are not guaranteed to be continuous at their end points, but a bezier curve is tangential at its end points, by putting the end points along a straight line, we guarantee that they have the same tangent and create a smooth curve.
  2. We're adding two vertices because, that same vertex needs to be (u,v) = (1,1) for the previous triangle and  (u,v) = (0,0) for the next one.
The code below does all this work, I've tweaked it a bit and added some additions like offsetting the vertices outwards from the rigibodies.
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
public class BlobManager : MonoBehaviour {
//Outer loop of the blob : Assign In Ediot
public Transform[] outerLoop;
//Center of the blob : Assign in editor
public Transform center;
//Adding some force internally to keep the blob "infated"
public float inflationIntensity;
//Offset the vertices outward slightly so they are closer to the outer edge
public float offsetFactor;
MeshFilter _mf ;
private Mesh blobMesh;
Vector2[] refUVs = { new Vector2(0,0), new Vector2(0,0.5f), new Vector2(1,1)};
// Use this for initialization
void Start () {
transform.position = Vector3.zero;
//Creating and adding a Mesh
blobMesh = new Mesh();
_mf = GetComponent<MeshFilter>();
//If not added in editor already
if(_mf == null){
this.gameObject.AddComponent<MeshFilter>();
}
MeshRenderer _mr = GetComponent<MeshRenderer>();
if( _mr == null ){
this.gameObject.AddComponent<MeshRenderer>();
}
int numVerts = 3*outerLoop.Length + 1;
int numTris = 2*outerLoop.Length*3;
List<Vector3> _vertices = new List<Vector3>(numVerts);
Vector2[] _uv = new Vector2[numVerts];
List<int> _triangles = new List<int>(numTris);
//Subdivide outer loop
int uvCtr = 0;
for( int it_outer = 0 ; it_outer < outerLoop.Length ; it_outer++){
//Circluar indexing on the outer loop
Vector3 curr = _offset(outerLoop[myMod( it_outer, outerLoop.Length)].position);
Vector3 next = _offset(outerLoop[myMod(it_outer + 1, outerLoop.Length)].position);
_vertices.Add(curr);
//Calculate and add double midpoints
Vector3 mid = (curr + next)/2.0f;
_vertices.Add(mid);
_vertices.Add(mid);
}
//Add the central vertex at the end
_vertices.Add(center.position);
//Generate triangles from computed vertices
int center_i = _vertices.Count - 1;
for( int it_outer = 0 ; it_outer < outerLoop.Length ; it_outer++){
//the tips of each traingle, because of the way we populated the array will be at indices 0,3,6, ....
int p1_i = 3 * it_outer;
//Circluat index to get the previous and next ertex
int p2_i = myMod(3 * it_outer + 1, _vertices.Count - 1);
int p3_i = myMod(3 * it_outer - 1, _vertices.Count -1);
//Add the triangles
_triangles.Add(p3_i);
_triangles.Add(p1_i);
_triangles.Add(p2_i);
//Add the central triangles
_triangles.Add(p2_i);
_triangles.Add(center_i);
_triangles.Add(p3_i);
//Set up UV's for the outer triangles
_uv[p3_i] = refUVs[0] ;
_uv[p1_i] = refUVs[1] ;
_uv[p2_i] = refUVs[2] ;
}
blobMesh.vertices = _vertices.ToArray();
blobMesh.uv = _uv;
blobMesh.triangles = _triangles.ToArray();
blobMesh.RecalculateNormals();
_mf.mesh = blobMesh;
Debug.Break();
}
//Circular indexing utility function
int myMod(int p1,int p2){
return (p1%p2 + p2)%p2;
}
//Utility vertex offsetting function
Vector3 _offset(Vector3 pt){
return pt + (pt-center.position)*offsetFactor;
}
// Update is called once per frame
void Update () {
Vector3[] _vertices = _mf.mesh.vertices;
//Update all the vertex positions every frame
for( int it_outer=0; it_outer< outerLoop.Length; it_outer++){
Vector3 curr = _offset(outerLoop[it_outer].position);
Vector3 next = _offset(outerLoop[(it_outer + 1)% outerLoop.Length].position);
_vertices[3*it_outer] = curr;
Vector3 mid = (curr + next)/2.0f;
_vertices[3*it_outer + 1 ] = mid;
_vertices[3*it_outer + 2] = mid;
Rigidbody2D _rbd = outerLoop[it_outer].gameObject.GetComponent<Rigidbody2D>();
Vector3 forceVec = curr - center.position;
//Force to inflate the blob;
_rbd.AddForce(forceVec * inflationIntensity);
}
_vertices[_vertices.Length - 1] = center.position;
_mf.mesh.vertices = _vertices;
}
}
view raw BlobManager.cs hosted with ❤ by GitHub

The Shader

Since I've already described the core logic of the shader earlier, for those that arent familiar, this is a overview of the OpenGL2 and DX9 rendering pipelines

An, important detail missed out in this image is that the Rasterizer( the GPU hardware) automagically interpolates color,uv and vertex-normal values within the pixels of the triangle, it is this interpolated data that you get at the fragment shader.

The final shader code is below.
Shader "Unlit/BezierShader"
{
Properties
{
_MainColor ("MainColor", Color) = (1,1,1,1)
}
SubShader
{
Tags { "Queue"="Transparent" }
Blend SrcAlpha OneMinusSrcAlpha
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
//Input data to the vertex program, vertex positiosn and u,v co-ordinates for each vertex
struct data
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
float4 _MainColor;
data vert (data v)
{
data o;
//USe the model-view-projection matrix to calculate screen space positions of the vertices
o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
o.uv = v.uv;
return o;
}
//Result from vertex program, is passed to fragment program
float4 frag (data i) : SV_Target
{
float4 col = _MainColor;
float2 p = i.uv;
//The condition we described earlier
float val = p.y*p.y - p.x;
//Do not render pixels outside
if (val > 0.05) discard;
//Some free anti-aliasing using an alpha of 0.5 of the edges
if( val >0 && val <= 0.05) col.a = 0.5;
//Point is inside return the main color.
return col;
//return float4(p.x,p.y,0,1);
}
ENDCG
}
}
}


Results

Now all you need to do is create the material, apply the shader and BOOM, the blob is transformed:


Up Next

Well, our mesh isn't manifold and has double vertices, plus we haven't allowed custom texture mapping of the blob yet, I have some ideas on how to achieve those goals using maybe some stencil buffering and masking.


Share this:

CONVERSATION

0 comments :

Post a Comment